diff --git a/docs/openapi.json b/docs/openapi.json index 85ce1fd0..3fc3ff12 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -182,6 +182,9 @@ }, { "name": "guides" + }, + { + "name": "plugin-endpoints" } ], "paths": { @@ -1784,6 +1787,183 @@ ] } }, + "/api/settings/asset-import/script": { + "get": { + "tags": [ + "core-identity" + ], + "summary": "Download Collect-ShopDB.ps1 with this site CSV path already embedded, so the collector script needs no hand editing...", + "description": "Download Collect-ShopDB.ps1 with this site CSV path already embedded, so the collector script needs no hand editing before it runs\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/script -o Collect-ShopDB.ps1\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/settings/asset-import/templates": { + "get": { + "tags": [ + "core-identity" + ], + "summary": "List the importable CSV tables in the order they must be uploaded - foreign keys mean the order is not optional", + "description": "List the importable CSV tables in the order they must be uploaded - foreign keys mean the order is not optional\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/settings/asset-import/templates/{tablename}": { + "get": { + "tags": [ + "core-identity" + ], + "summary": "Download a generated CSV template for one importable table, with its real column names as the header row", + "description": "Download a generated CSV template for one importable table, with its real column names as the header row\n\n**Auth:** admin role\n\n**Params:** path: tablename\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates/assets -o assets.csv\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "tablename", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/settings/asset-import/import": { + "post": { + "tags": [ + "core-identity" + ], + "summary": "Validate uploaded CSVs and, only when commit=true, load them. The default is a dry run that reports what WOULD change...", + "description": "Validate uploaded CSVs and, only when commit=true, load them. The default is a dry run that reports what WOULD change; a committing run writes an AuditLog row with the reconciled count\n\n**Auth:** admin role\n\n**Params:** multipart: one file per table; form: commit (false = validate only)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -F \"assets=@assets.csv\" -F \"commit=true\" http://localhost:5001/api/settings/asset-import/import\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "multipart: one file per table; form: commit (false = validate only)" + } + } + } + } + } + }, "/api/reports": { "get": { "tags": [ @@ -10057,6 +10237,589 @@ } } }, + "/api/computers/policies/configuration": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "List Configuration Policies (scope + the actions applied to matching endpoints); 404 while the...", + "description": "List Configuration Policies (scope + the actions applied to matching endpoints); 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-computers" + ], + "summary": "Create a Configuration Policy. Scope trees go through validate_scope and every action name is checked against the...", + "description": "Create a Configuration Policy. Scope trees go through validate_scope and every action name is checked against the endpoints command allowlist, so a bad tree or unknown action is rejected before it reaches the database; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.create\n\n**Params:** body: name (required), description, scopejson (scope AST), actions[] of {action, mode: desired|gate, params}\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"name\":\"Shopfloor MTConnect\",\"actions\":[{\"action\":\"mtconnect_start\",\"mode\":\"desired\"}]}' http://localhost:5001/api/computers/policies/configuration\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: name (required), description, scopejson (scope AST), actions[] of {action, mode: desired|gate, params}" + } + } + } + } + } + }, + "/api/computers/policies/configuration/{policy_id}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "One Configuration Policy with its actions expanded; 404 while the computers_configpolicies_enabled setting is off", + "description": "One Configuration Policy with its actions expanded; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "Update a Configuration Policy; same scope and action validation as create; 404 while the...", + "description": "Update a Configuration Policy; same scope and action validation as create; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.edit\n\n**Params:** path: policy_id; body: any of name, description, scopejson, actions[] (actions replace wholesale)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: policy_id; body: any of name, description, scopejson, actions[] (actions replace wholesale)" + } + } + } + } + }, + "delete": { + "tags": [ + "plugin-computers" + ], + "summary": "Delete a Configuration Policy and its actions; 404 while the computers_configpolicies_enabled setting is off", + "description": "Delete a Configuration Policy and its actions; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.delete\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/computers/policies/business": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "List Business Policies (ordered bundles of Configuration Policies); 404 while the computers_configpolicies_enabled...", + "description": "List Business Policies (ordered bundles of Configuration Policies); 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-computers" + ], + "summary": "Create a Business Policy. Every referenced Configuration Policy must exist; priority decides which policy wins when two...", + "description": "Create a Business Policy. Every referenced Configuration Policy must exist; priority decides which policy wins when two match the same endpoint; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.create\n\n**Params:** body: name (required), description, priority, items[] of {configurationpolicyid, sortorder}\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: name (required), description, priority, items[] of {configurationpolicyid, sortorder}" + } + } + } + } + } + }, + "/api/computers/policies/business/{policy_id}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "One Business Policy with its member Configuration Policies in sort order; 404 while the...", + "description": "One Business Policy with its member Configuration Policies in sort order; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "Update a Business Policy or reorder its members; 404 while the computers_configpolicies_enabled setting is off", + "description": "Update a Business Policy or reorder its members; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.edit\n\n**Params:** path: policy_id; body: any of name, description, priority, items[] (items replace wholesale)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: policy_id; body: any of name, description, priority, items[] (items replace wholesale)" + } + } + } + } + }, + "delete": { + "tags": [ + "plugin-computers" + ], + "summary": "Delete a Business Policy and its item rows; 404 while the computers_configpolicies_enabled setting is off", + "description": "Delete a Business Policy and its item rows; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.delete\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/computers/policies/actions": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "The allowlisted action names and display labels a policy may reference, read from the endpoints command allowlist. The...", + "description": "The allowlisted action names and display labels a policy may reference, read from the endpoints command allowlist. The UI builds its action picker from this rather than hardcoding a list that would drift; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/actions\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/computers/policies/effective/{asset_id}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies...", + "description": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies matched, and the resulting desired/gate actions. Read-only, for debugging a scope that is not matching what its author expected; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/effective/1asset_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/measuringtools/types": { "get": { "tags": [ @@ -20969,6 +21732,852 @@ } ] } + }, + "/api/endpoints/": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List endpoints reporting through the ShopDB agent, with last-seen and online state", + "description": "List endpoints reporting through the ShopDB agent, with last-seen and online state\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/{asset_id}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "One endpoint: its agent state, version, and last check-in", + "description": "One endpoint: its agent state, version, and last check-in\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/{asset_id}/events": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent event history for one endpoint, newest first", + "description": "Agent event history for one endpoint, newest first\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id; query: eventid (page from this id)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/events\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/status": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: online state, uptime, and agent version for one endpoint", + "description": "Asset-detail panel: online state, uptime, and agent version for one endpoint\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/status\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/telemetrymeta": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: what telemetry this endpoint reports and when each series was last updated", + "description": "Asset-detail panel: what telemetry this endpoint reports and when each series was last updated\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/telemetrymeta\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/services": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: Windows services the agent watches on this endpoint, with running state", + "description": "Asset-detail panel: Windows services the agent watches on this endpoint, with running state\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/services\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/recentevents": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: the most recent agent events for one endpoint", + "description": "Asset-detail panel: the most recent agent events for one endpoint\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/recentevents\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/interfaces": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: network interfaces as the collector reports them ({alias, ipv4, mac, gateway, subnet, dhcp})", + "description": "Asset-detail panel: network interfaces as the collector reports them ({alias, ipv4, mac, gateway, subnet, dhcp})\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/interfaces\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/dashboard/summary": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Dashboard card: endpoint counts by online/offline/never-seen", + "description": "Dashboard card: endpoint counts by online/offline/never-seen\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/summary\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/dashboard/quiet": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Dashboard card: endpoints that have gone quiet - no check-in inside the configured quiet-hours window", + "description": "Dashboard card: endpoints that have gone quiet - no check-in inside the configured quiet-hours window\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/quiet\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/dashboard/stability": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Dashboard card: endpoints ranked by reboot/agent-restart churn, to surface a machine that is flapping", + "description": "Dashboard card: endpoints ranked by reboot/agent-restart churn, to surface a machine that is flapping\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/stability\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/agentkey": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Status of the endpoints agent key: whether one exists, when it was issued, and its last use. Never returns the key...", + "description": "Status of the endpoints agent key: whether one exists, when it was issued, and its last use. Never returns the key itself\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "delete": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Revoke the endpoints agent key. The kill switch for agent ingest: reports and command polls stop being authorized...", + "description": "Revoke the endpoints agent key. The kill switch for agent ingest: reports and command polls stop being authorized immediately\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/agentkey/regenerate": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Issue a new endpoints agent key and return it ONCE. The key is scoped to collector.ingest.endpoints only, so rotating...", + "description": "Issue a new endpoints agent key and return it ONCE. The key is scoped to collector.ingest.endpoints only, so rotating it cannot disturb the computers or GE-Enforce collectors. Every agent must be reconfigured with the new value\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey/regenerate\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "none" + } + } + } + } + } + }, + "/api/endpoints/{asset_id}/commands": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Command history for one endpoint: what was queued, claimed, and what it returned", + "description": "Command history for one endpoint: what was queued, claimed, and what it returned\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/commands\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Queue one allowlisted command for an endpoint. Unknown actions are rejected, destructive ones (system_restart) demand a...", + "description": "Queue one allowlisted command for an endpoint. Unknown actions are rejected, destructive ones (system_restart) demand a stricter permission, the effective policy must permit the action for this endpoint, and a second command will not stack while one is pending or running\n\n**Auth:** endpoints.command\n\n**Params:** path: asset_id; body: action (required, allowlisted), target, targetkind\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"action\":\"service_restart\",\"target\":\"MTConnectAgent\",\"targetkind\":\"service\"}' http://localhost:5001/api/endpoints/1/commands\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id; body: action (required, allowlisted), target, targetkind" + } + } + } + } + } + }, + "/api/endpoints/commands/next": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent long-poll. Claims the oldest pending command for the calling host and atomically marks it running so no second...", + "description": "Agent long-poll. Claims the oldest pending command for the calling host and atomically marks it running so no second poll claims it; 204 when nothing is queued inside the hold window, and the agent re-polls. Collector-token auth, not JWT\n\n**Auth:** collector token (collector.ingest.endpoints)\n\n**Params:** query: hostname (required)\n\n**Example:**\n```\ncurl -H \"X-Collector-Key: $KEY\" \"http://localhost:5001/api/endpoints/commands/next?hostname=WJSF-PC-01\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/commands/{command_id}/result": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state...", + "description": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state, caches that on the EndpointState so the UI toggle reflects reality between polls - both in one commit. Collector-token auth, not JWT\n\n**Auth:** collector token (collector.ingest.endpoints)\n\n**Params:** path: command_id; body: the command outcome (status, output)\n\n**Example:**\n```\ncurl -X POST -H \"X-Collector-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"status\":\"succeeded\",\"output\":\"ok\"}' http://localhost:5001/api/endpoints/commands/1/result\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "command_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: command_id; body: the command outcome (status, output)" + } + } + } + } + } } } } \ No newline at end of file diff --git a/plugins/computers/migrations/versions/0004_policies.py b/plugins/computers/migrations/versions/0004_policies.py index d5c7f737..4a78c1cb 100644 --- a/plugins/computers/migrations/versions/0004_policies.py +++ b/plugins/computers/migrations/versions/0004_policies.py @@ -22,6 +22,14 @@ branch_labels = None depends_on = None +# TEXT rather than sa.JSON on purpose. MySQL 5.6 has no JSON type - it arrived +# in 5.7 - and this migration failed there with a bare syntax error near the +# word JSON. Every other JSON-bearing column in the product is TEXT for the same +# reason (guides.blocksjson, geenforce.manifestjson); the model reads and writes +# dicts through the JSONText type decorator, so nothing above the database +# changes. + + def upgrade(): bind = op.get_bind() insp = sa.inspect(bind) @@ -33,7 +41,7 @@ def upgrade(): sa.Column('configurationpolicyid', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=150), nullable=False), sa.Column('description', sa.Text(), nullable=True), - sa.Column('scopejson', sa.JSON(), nullable=True), + sa.Column('scopejson', sa.Text(), nullable=True), sa.Column('source', sa.String(length=50), nullable=False, server_default='local'), sa.Column('createddate', sa.DateTime(), nullable=False), @@ -57,8 +65,8 @@ def upgrade(): sa.Column('action', sa.String(length=100), nullable=False), sa.Column('mode', sa.String(length=20), nullable=False, server_default='gate'), - sa.Column('desiredstate', sa.JSON(), nullable=True), - sa.Column('params', sa.JSON(), nullable=True), + sa.Column('desiredstate', sa.Text(), nullable=True), + sa.Column('params', sa.Text(), nullable=True), sa.Column('intervalminutes', sa.Integer(), nullable=True), sa.Column('sortorder', sa.Integer(), nullable=False, server_default='0'), diff --git a/plugins/computers/models/jsontext.py b/plugins/computers/models/jsontext.py new file mode 100644 index 00000000..82f476e3 --- /dev/null +++ b/plugins/computers/models/jsontext.py @@ -0,0 +1,44 @@ +"""JSON stored in a TEXT column. + +MySQL 5.6 has no JSON type - it arrived in 5.7 - and a site is still running +5.6, where `sa.JSON()` fails at migration time with a bare syntax error near the +word JSON. Every other JSON-bearing column in this product is already TEXT for +that reason: guides' `blocksjson`, GE-Enforce's `manifestjson`. + +This keeps the Python side identical. A model attribute still holds a dict or a +list, still assigns and reads like one; only the storage changes. The +alternative - json.dumps and json.loads at seventeen call sites - is the same +thing written out seventeen times, with seventeen chances to forget one. + +Note the SQLite behaviour is deliberately the same as MySQL's. Tests run on +SQLite and production does not, so a type that behaved differently across the +two would hide exactly the failure this exists to prevent. +""" + +import json + +from sqlalchemy import Text +from sqlalchemy.types import TypeDecorator + + +class JSONText(TypeDecorator): + """A dict or list on the Python side, TEXT in the database.""" + + impl = Text + cache_ok = True + + def process_bind_param(self, value, dialect): + if value is None: + return None + return json.dumps(value, separators=(',', ':')) + + def process_result_value(self, value, dialect): + if value is None or value == '': + return None + try: + return json.loads(value) + except (TypeError, ValueError): + # A row written before this column was JSON, or hand-edited. Hand + # back the raw text rather than raising: a policy that cannot be + # parsed should be visible and fixable, not an exception on read. + return value diff --git a/plugins/computers/models/policy.py b/plugins/computers/models/policy.py index a95ea5a4..1896a9ac 100644 --- a/plugins/computers/models/policy.py +++ b/plugins/computers/models/policy.py @@ -18,6 +18,7 @@ every table for a clean rollback. """ from shopdb.api import db, BaseModel +from .jsontext import JSONText # Provenance of a policy row. @@ -43,7 +44,7 @@ class ConfigurationPolicy(BaseModel): # Boolean target AST: {"type":"and","nodes":[{"type":"pctype","id":3}, # {"type":"not","node":{"type":"app","id":12}}]}. Validated server-side # (known node types, depth cap) before it is stored - see policyscope. - scopejson = db.Column(db.JSON, nullable=True) + scopejson = db.Column(JSONText, nullable=True) # GE-Enforce interop seam (see POLICY_SOURCE_LOCAL). source = db.Column(db.String(50), default=POLICY_SOURCE_LOCAL, nullable=False) @@ -101,11 +102,11 @@ class ConfigurationPolicyAction(BaseModel): # Desired-state target for mode=desired, as JSON so different actions can # express different targets (e.g. {"running": true}). Null for gate mode. - desiredstate = db.Column(db.JSON, nullable=True) + desiredstate = db.Column(JSONText, nullable=True) # Optional action parameters passed through to the command (JSON), and an # optional reconcile cadence in minutes (null = reconcile on every report). - params = db.Column(db.JSON, nullable=True) + params = db.Column(JSONText, nullable=True) intervalminutes = db.Column(db.Integer, nullable=True) sortorder = db.Column(db.Integer, default=0, nullable=False)