{ "openapi": "3.0.3", "info": { "title": "Dockhand API", "version": "1.0.45", "description": "Auto-generated from src/routes/**/+server.ts by scripts/generate-openapi.ts. Path, HTTP method, tag, and auth requirement are derived automatically from the route tree and hooks.server.ts PUBLIC_PATHS. Parameters, response status codes, and request body fields are additionally auto-detected from each handler’s own source (query params via url.searchParams, status codes via status:/error(), body fields via destructuring) — adding a new endpoint therefore requires ZERO manual spec edits to show up with real params/responses. An optional, additive `@openapi` JSDoc annotation on a handler replaces the generic auto-descriptions with hand-written summaries, exact types, and examples." }, "servers": [ { "url": "/" } ], "tags": [ { "name": "activity" }, { "name": "audit" }, { "name": "auth" }, { "name": "auto-update" }, { "name": "backup" }, { "name": "batch" }, { "name": "changelog" }, { "name": "config-sets" }, { "name": "container-icons" }, { "name": "containers" }, { "name": "dashboard" }, { "name": "debug" }, { "name": "dependencies" }, { "name": "docs" }, { "name": "environments" }, { "name": "events" }, { "name": "git" }, { "name": "hawser" }, { "name": "health" }, { "name": "host" }, { "name": "icons" }, { "name": "images" }, { "name": "jobs" }, { "name": "labels" }, { "name": "legal" }, { "name": "license" }, { "name": "logs" }, { "name": "metrics" }, { "name": "networks" }, { "name": "notifications" }, { "name": "preferences" }, { "name": "profile" }, { "name": "prune" }, { "name": "registries" }, { "name": "registry" }, { "name": "roles" }, { "name": "schedules" }, { "name": "secret-providers" }, { "name": "self-update" }, { "name": "settings" }, { "name": "stacks" }, { "name": "system" }, { "name": "templates" }, { "name": "users" }, { "name": "volumes" }, { "name": "vulnerabilities" } ], "components": { "securitySchemes": { "cookieAuth": { "type": "apiKey", "in": "cookie", "name": "dockhand_session", "description": "Session cookie set on login (src/lib/server/auth.ts validateSession)." }, "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "dh_<43-char base64url>", "description": "User-scoped API token (src/lib/server/api-tokens.ts). Only evaluated on /api/* and /metrics when no session cookie is present (src/hooks.server.ts). Rate-limited: 10 failures/IP -> 429 for 5 minutes." } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "paths": { "/api/activity": { "get": { "operationId": "get_api_activity", "tags": [ "activity" ], "summary": "Query container activity events with filters and pagination", "parameters": [ { "name": "environmentId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter to a single environment (from GET /api/environments)" }, { "name": "containerId", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by container ID (from GET /api/containers)" }, { "name": "containerName", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by container name" }, { "name": "actions", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated event actions to filter by" }, { "name": "labels", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated labels to filter by" }, { "name": "fromDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Start of the date range (ISO 8601)" }, { "name": "toDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "End of the date range (ISO 8601)" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Maximum number of events to return" }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Number of events to skip (pagination)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "events": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "containerName": { "type": "string" }, "action": { "type": "string" }, "timestamp": { "type": "string" } }, "required": [ "id" ] } }, "total": { "type": "integer" }, "limit": { "type": "integer" }, "offset": { "type": "integer" } }, "required": [ "total", "limit", "offset" ] }, "example": { "events": [ { "id": 0, "containerName": "string", "action": "string", "timestamp": "string" } ], "total": 0, "limit": 0, "offset": 0 } } } }, "403": { "description": "Permission denied (requires the activity:view permission)" }, "500": { "description": "Failed to fetch container events" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "operationId": "delete_api_activity", "tags": [ "activity" ], "summary": "Clear all stored container activity events", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (requires the activity:delete permission)" }, "500": { "description": "Failed to clear container events" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/activity/containers": { "get": { "operationId": "get_api_activity_containers", "tags": [ "activity" ], "summary": "List distinct container names that appear in the activity log, for filter dropdowns", "parameters": [ { "name": "environment_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter to a single environment (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } }, "example": [ "web-1", "db-1", "cache-1" ] } } }, "403": { "description": "Permission denied (requires the activity:view permission)" }, "500": { "description": "Failed to fetch container names" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/activity/events": { "get": { "operationId": "get_api_activity_events", "tags": [ "activity" ], "summary": "Stream live container activity and environment-status events over Server-Sent Events", "parameters": [], "responses": { "200": { "description": "Server-Sent Events stream (text/event-stream) emitting connected, heartbeat, activity and env_status events" }, "403": { "description": "Permission denied (requires the activity:view permission)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/activity/stats": { "get": { "operationId": "get_api_activity_stats", "tags": [ "activity" ], "summary": "Get aggregate container activity statistics (totals and counts by action)", "parameters": [ { "name": "environment_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter to a single environment (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "total": { "type": "integer" }, "today": { "type": "integer" }, "byAction": { "type": "object", "properties": {} } }, "required": [ "total", "today" ] }, "example": { "total": 128, "today": 7, "byAction": { "start": 40, "stop": 30 } } } } }, "403": { "description": "Permission denied (requires the activity:view permission)" }, "500": { "description": "Failed to fetch stats" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/audit": { "get": { "operationId": "get_api_audit", "tags": [ "audit" ], "summary": "Query the audit log with filters and pagination (Enterprise only)", "parameters": [ { "name": "usernames", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated usernames to filter by" }, { "name": "entityTypes", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated entity types to filter by" }, { "name": "actions", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated actions to filter by" }, { "name": "username", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Legacy single-username filter" }, { "name": "entityType", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Legacy single entity-type filter" }, { "name": "action", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Legacy single-action filter" }, { "name": "environmentId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter to a single environment (from GET /api/environments)" }, { "name": "labels", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated labels to filter by" }, { "name": "fromDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Start of the date range (ISO 8601)" }, { "name": "toDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "End of the date range (ISO 8601)" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Maximum number of entries to return" }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Number of entries to skip (pagination)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "logs": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "action": { "type": "string" }, "entityType": { "type": "string" }, "entityName": { "type": "string" }, "createdAt": { "type": "string" } }, "required": [ "id" ] } }, "total": { "type": "integer" } } }, "example": { "logs": [ { "id": 0, "username": "string", "action": "string", "entityType": "string", "entityName": "string", "createdAt": "string" } ], "total": 0 } } } }, "403": { "description": "Enterprise required, or permission denied" }, "500": { "description": "Failed to fetch audit logs" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/audit/events": { "get": { "operationId": "get_api_audit_events", "tags": [ "audit" ], "summary": "Stream live audit-log events over Server-Sent Events (Enterprise only)", "parameters": [], "responses": { "200": { "description": "Server-Sent Events stream (text/event-stream) emitting connected, heartbeat and audit events" }, "403": { "description": "Enterprise required, or permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/audit/export": { "get": { "operationId": "get_api_audit_export", "tags": [ "audit" ], "summary": "Export the audit log as a downloadable file in JSON, CSV or Markdown (Enterprise only)", "parameters": [ { "name": "username", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by username" }, { "name": "entityType", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by entity type" }, { "name": "action", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by action" }, { "name": "environmentId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter to a single environment (from GET /api/environments)" }, { "name": "fromDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Start of the date range (ISO 8601)" }, { "name": "toDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "End of the date range (ISO 8601)" }, { "name": "format", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Output format — json (default), csv or md" } ], "responses": { "200": { "description": "Downloadable audit-log export (Content-Disposition attachment; JSON, CSV or Markdown)" }, "403": { "description": "Enterprise required, or permission denied" }, "500": { "description": "Failed to export audit logs" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/audit/users": { "get": { "operationId": "get_api_audit_users", "tags": [ "audit" ], "summary": "List the distinct usernames that appear in the audit log, for filter dropdowns (Enterprise only)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } }, "example": [ "admin", "ci-bot", "alice" ] } } }, "403": { "description": "Enterprise required, or permission denied" }, "500": { "description": "Failed to fetch audit log users" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/auth/ldap": { "get": { "operationId": "get_api_auth_ldap", "tags": [ "auth" ], "summary": "List all configured LDAP providers (enterprise only; bind passwords are masked)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "serverUrl": { "type": "string" }, "baseDn": { "type": "string" } }, "required": [ "id", "name", "enabled", "serverUrl", "baseDn" ] } }, "example": [ { "id": 1, "name": "Corporate LDAP", "enabled": true, "serverUrl": "ldaps://ldap.example.com:636", "baseDn": "dc=example,dc=com" } ] } } }, "401": { "description": "Authentication required (auth is enabled and the caller is not an authenticated admin)" }, "403": { "description": "Enterprise license required" }, "500": { "description": "Failed to read the LDAP configurations" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_auth_ldap", "tags": [ "auth" ], "summary": "Create a new LDAP provider configuration (enterprise only)", "parameters": [], "responses": { "201": { "description": "LDAP configuration created (bindPassword is masked in the response)", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "serverUrl": { "type": "string" }, "baseDn": { "type": "string" } }, "required": [ "id", "name", "enabled", "serverUrl", "baseDn" ] }, "example": { "id": 0, "name": "string", "enabled": true, "serverUrl": "string", "baseDn": "string" } } } }, "400": { "description": "Name, server URL, and base DN are required" }, "401": { "description": "Authentication required (auth is enabled and the caller is not an authenticated admin)" }, "403": { "description": "Enterprise license required" }, "500": { "description": "Failed to create the LDAP configuration" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "serverUrl": { "type": "string" }, "baseDn": { "type": "string" }, "enabled": { "type": "boolean" }, "bindDn": { "type": "string" }, "bindPassword": { "type": "string" }, "userFilter": { "type": "string" }, "usernameAttribute": { "type": "string" }, "emailAttribute": { "type": "string" }, "displayNameAttribute": { "type": "string" }, "groupBaseDn": { "type": "string" }, "groupFilter": { "type": "string" }, "adminGroup": { "type": "string" }, "roleMappings": { "type": "object", "properties": {} }, "tlsEnabled": { "type": "boolean" }, "tlsCa": { "type": "string" } }, "required": [ "name", "serverUrl", "baseDn" ] }, "example": { "name": "Corporate LDAP", "serverUrl": "ldaps://ldap.example.com:636", "baseDn": "dc=example,dc=com", "bindDn": "cn=admin,dc=example,dc=com", "bindPassword": "***", "enabled": true, "tlsEnabled": true } } } } } }, "/api/auth/ldap/{id}": { "get": { "operationId": "get_api_auth_ldap_id", "tags": [ "auth" ], "summary": "Get a single LDAP provider configuration by id (enterprise only; bind password is masked)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the LDAP configuration (from GET /api/auth/ldap)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "serverUrl": { "type": "string" }, "baseDn": { "type": "string" } }, "required": [ "id", "name", "enabled", "serverUrl", "baseDn" ] }, "example": { "id": 0, "name": "string", "enabled": true, "serverUrl": "string", "baseDn": "string" } } } }, "400": { "description": "Invalid id (not a number)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not an authenticated admin)" }, "403": { "description": "Enterprise license required" }, "404": { "description": "LDAP configuration not found" }, "500": { "description": "Failed to read the LDAP configuration" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_auth_ldap_id", "tags": [ "auth" ], "summary": "Update an existing LDAP provider configuration (only supplied fields change; a masked bindPassword is ignored)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the LDAP configuration (from GET /api/auth/ldap)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "serverUrl": { "type": "string" }, "baseDn": { "type": "string" } }, "required": [ "id", "name", "enabled", "serverUrl", "baseDn" ] }, "example": { "id": 0, "name": "string", "enabled": true, "serverUrl": "string", "baseDn": "string" } } } }, "400": { "description": "Invalid id (not a number)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not an authenticated admin)" }, "403": { "description": "Enterprise license required" }, "404": { "description": "LDAP configuration not found" }, "500": { "description": "Failed to update the LDAP configuration" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "enabled": { "type": "boolean" }, "serverUrl": { "type": "string" }, "bindDn": { "type": "string" }, "bindPassword": { "type": "string" }, "baseDn": { "type": "string" }, "userFilter": { "type": "string" }, "usernameAttribute": { "type": "string" }, "emailAttribute": { "type": "string" }, "displayNameAttribute": { "type": "string" }, "groupBaseDn": { "type": "string" }, "groupFilter": { "type": "string" }, "adminGroup": { "type": "string" }, "roleMappings": { "type": "object", "properties": {} }, "tlsEnabled": { "type": "boolean" }, "tlsCa": { "type": "string" } } }, "example": { "enabled": false, "userFilter": "(sAMAccountName={{username}})", "bindPassword": "***" } } } } }, "delete": { "operationId": "delete_api_auth_ldap_id", "tags": [ "auth" ], "summary": "Delete an LDAP provider configuration by id (enterprise only)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the LDAP configuration (from GET /api/auth/ldap)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid id (not a number)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not an authenticated admin)" }, "403": { "description": "Enterprise license required" }, "404": { "description": "LDAP configuration not found" }, "500": { "description": "Failed to delete the LDAP configuration" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/auth/ldap/{id}/test": { "post": { "operationId": "post_api_auth_ldap_id_test", "tags": [ "auth" ], "summary": "Test connectivity of a stored LDAP configuration by id (enterprise only)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the LDAP configuration to test (from GET /api/auth/ldap)" } ], "responses": { "200": { "description": "Connection test result (success flag plus diagnostic detail from the LDAP server)" }, "400": { "description": "Invalid id (not a number)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not an authenticated admin)" }, "403": { "description": "Enterprise license required" }, "404": { "description": "LDAP configuration not found" }, "500": { "description": "Failed to test the LDAP connection" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/auth/login": { "post": { "operationId": "post_api_auth_login", "tags": [ "auth" ], "summary": "Authenticate with username/password (local or LDAP) and set the session cookie", "parameters": [], "responses": { "200": { "description": "Login succeeded and dockhand_session cookie was set — OR requiresMfa:true if a second factor is needed first", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "user": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "isAdmin": { "type": "boolean" } }, "required": [ "id", "username", "isAdmin" ] } }, "required": [ "success" ] }, "example": { "success": true, "user": { "id": 1, "username": "admin", "email": "admin@example.com", "displayName": "Admin", "isAdmin": true } } } } }, "400": { "description": "Authentication disabled, or username/password missing" }, "401": { "description": "Invalid credentials or invalid MFA code" }, "403": { "description": "Local login disabled via DISABLE_LOCAL_LOGIN" }, "429": { "description": "Rate-limited (too many attempts for this IP+username)" }, "500": { "description": "Unexpected error during authentication" } }, "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "username": { "type": "string" }, "password": { "type": "string" }, "mfaToken": { "type": "string" }, "provider": { "type": "string" } }, "required": [ "username", "password" ] }, "example": { "username": "admin", "password": "correct horse battery staple", "provider": "local" } } } } } }, "/api/auth/logout": { "post": { "operationId": "post_api_auth_logout", "tags": [ "auth" ], "summary": "Destroy the current session (clears the dockhand_session cookie)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "500": { "description": "Unexpected error while destroying the session" } }, "security": [] } }, "/api/auth/oidc": { "get": { "operationId": "get_api_auth_oidc", "tags": [ "auth" ], "summary": "List all configured OIDC providers (client secrets are masked)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "issuerUrl": { "type": "string" }, "clientId": { "type": "string" } }, "required": [ "id", "name", "enabled", "issuerUrl", "clientId" ] } }, "example": [ { "id": 1, "name": "Authentik", "enabled": true, "issuerUrl": "https://auth.example.com/application/o/dockhand/", "clientId": "dockhand" } ] } } }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing settings:view)" }, "500": { "description": "Failed to read the OIDC configurations" } }, "security": [] }, "post": { "operationId": "post_api_auth_oidc", "tags": [ "auth" ], "summary": "Create a new OIDC provider configuration", "parameters": [], "responses": { "201": { "description": "OIDC configuration created (clientSecret is masked in the response)", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "issuerUrl": { "type": "string" }, "clientId": { "type": "string" } }, "required": [ "id", "name", "enabled", "issuerUrl", "clientId" ] }, "example": { "id": 0, "name": "string", "enabled": true, "issuerUrl": "string", "clientId": "string" } } } }, "400": { "description": "A required field is missing (name, issuerUrl, clientId, clientSecret, redirectUri)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing settings:edit)" }, "500": { "description": "Failed to create the OIDC configuration" } }, "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "issuerUrl": { "type": "string" }, "clientId": { "type": "string" }, "clientSecret": { "type": "string" }, "redirectUri": { "type": "string" }, "enabled": { "type": "boolean" }, "scopes": { "type": "string" }, "usernameClaim": { "type": "string" }, "emailClaim": { "type": "string" }, "displayNameClaim": { "type": "string" }, "adminClaim": { "type": "string" }, "adminValue": { "type": "string" }, "roleMappingsClaim": { "type": "string" }, "roleMappings": { "type": "object", "properties": {} } }, "required": [ "name", "issuerUrl", "clientId", "clientSecret", "redirectUri" ] }, "example": { "name": "Authentik", "issuerUrl": "https://auth.example.com/application/o/dockhand/", "clientId": "dockhand", "clientSecret": "***", "redirectUri": "https://dockhand.example.com/api/auth/oidc/callback", "enabled": true, "scopes": "openid profile email" } } } } } }, "/api/auth/oidc/{id}": { "get": { "operationId": "get_api_auth_oidc_id", "tags": [ "auth" ], "summary": "Get a single OIDC provider configuration by id (client secret is masked)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the OIDC configuration (from GET /api/auth/oidc)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "issuerUrl": { "type": "string" }, "clientId": { "type": "string" } }, "required": [ "id", "name", "enabled", "issuerUrl", "clientId" ] }, "example": { "id": 0, "name": "string", "enabled": true, "issuerUrl": "string", "clientId": "string" } } } }, "400": { "description": "Invalid configuration id (not a number)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing settings:view)" }, "404": { "description": "OIDC configuration not found" }, "500": { "description": "Failed to read the OIDC configuration" } }, "security": [] }, "put": { "operationId": "put_api_auth_oidc_id", "tags": [ "auth" ], "summary": "Update an existing OIDC provider configuration (only supplied fields change; a masked clientSecret is ignored)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the OIDC configuration (from GET /api/auth/oidc)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "issuerUrl": { "type": "string" }, "clientId": { "type": "string" } }, "required": [ "id", "name", "enabled", "issuerUrl", "clientId" ] }, "example": { "id": 0, "name": "string", "enabled": true, "issuerUrl": "string", "clientId": "string" } } } }, "400": { "description": "Invalid configuration id (not a number)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing settings:edit)" }, "404": { "description": "OIDC configuration not found" }, "500": { "description": "Failed to update the OIDC configuration" } }, "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "enabled": { "type": "boolean" }, "issuerUrl": { "type": "string" }, "clientId": { "type": "string" }, "clientSecret": { "type": "string" }, "redirectUri": { "type": "string" }, "scopes": { "type": "string" }, "usernameClaim": { "type": "string" }, "emailClaim": { "type": "string" }, "displayNameClaim": { "type": "string" }, "adminClaim": { "type": "string" }, "adminValue": { "type": "string" }, "roleMappingsClaim": { "type": "string" }, "roleMappings": { "type": "object", "properties": {} } } }, "example": { "enabled": true, "scopes": "openid profile email groups", "clientSecret": "***" } } } } }, "delete": { "operationId": "delete_api_auth_oidc_id", "tags": [ "auth" ], "summary": "Delete an OIDC provider configuration by id", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the OIDC configuration (from GET /api/auth/oidc)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid configuration id (not a number)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing settings:edit)" }, "404": { "description": "OIDC configuration not found" }, "500": { "description": "Failed to delete the OIDC configuration" } }, "security": [] } }, "/api/auth/oidc/{id}/initiate": { "get": { "operationId": "get_api_auth_oidc_id_initiate", "tags": [ "auth" ], "summary": "Start the OIDC login flow for a provider — on success throws a 302 redirect to the IdP authorization URL", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the OIDC provider (from GET /api/auth/oidc)" }, { "name": "redirect", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Post-login destination path to return to (defaults to /)" } ], "responses": { "200": { "description": "Successful response" }, "302": { "description": "Redirect to the IdP's authorization URL" }, "400": { "description": "Authentication is not enabled, or the configuration id is invalid" }, "404": { "description": "OIDC provider not found or disabled" }, "500": { "description": "Failed to build the authorization URL / initiate SSO" } }, "security": [] }, "post": { "operationId": "post_api_auth_oidc_id_initiate", "tags": [ "auth" ], "summary": "Return the OIDC authorization URL for a provider without redirecting (JSON response)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the OIDC provider (from GET /api/auth/oidc)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string" } }, "required": [ "url" ] }, "example": { "url": "string" } } } }, "400": { "description": "Authentication is not enabled, or the configuration id is invalid" }, "404": { "description": "OIDC provider not found or disabled" }, "500": { "description": "Failed to build the authorization URL / initiate SSO" } }, "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "redirect": { "type": "string" } } }, "example": { "redirect": "/dashboard" } } } } } }, "/api/auth/oidc/{id}/test": { "post": { "operationId": "post_api_auth_oidc_id_test", "tags": [ "auth" ], "summary": "Test the discovery/connection of a stored OIDC configuration by id", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the OIDC configuration to test (from GET /api/auth/oidc)" } ], "responses": { "200": { "description": "Connection test result (success flag plus diagnostic detail from the provider)" }, "400": { "description": "Invalid configuration id (not a number)" }, "403": { "description": "Admin access required (auth is enabled and the caller is not an admin)" }, "500": { "description": "Failed to test the OIDC connection" } }, "security": [] } }, "/api/auth/oidc/callback": { "get": { "operationId": "get_api_auth_oidc_callback", "tags": [ "auth" ], "summary": "Handle the OIDC redirect callback from the IdP — always responds with a 302 redirect (to the original destination on success, or to /login with an error query param on failure)", "parameters": [ { "name": "code", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Authorization code returned by the IdP" }, { "name": "state", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Opaque state value used to correlate the request and carry the post-login redirect" }, { "name": "error", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Error code returned by the IdP when authentication failed" }, { "name": "error_description", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Human-readable error detail returned by the IdP" } ], "responses": { "200": { "description": "Successful response" }, "302": { "description": "Always a redirect — on success a session cookie is set and the caller is redirected to the original destination; on any error the caller is redirected to /login with an error query param" } }, "security": [] } }, "/api/auth/providers": { "get": { "operationId": "get_api_auth_providers", "tags": [ "auth" ], "summary": "List the authentication providers offered on the login page (local, LDAP, OIDC), plus the default provider", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "providers": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "type": { "type": "string" }, "initiateUrl": { "type": "string" } }, "required": [ "id", "name", "type" ] } }, "defaultProvider": { "type": "string" } } }, "example": { "providers": [ { "id": "local", "name": "Local", "type": "local" }, { "id": "oidc:1", "name": "Authentik", "type": "oidc", "initiateUrl": "/api/auth/oidc/1/initiate" } ], "defaultProvider": "local" } } } } }, "security": [] } }, "/api/auth/session": { "get": { "operationId": "get_api_auth_session", "tags": [ "auth" ], "summary": "Get the current session (public — used by the frontend to bootstrap auth state)", "parameters": [], "responses": { "200": { "description": "user is present only when authenticated:true", "content": { "application/json": { "schema": { "type": "object", "properties": { "authenticated": { "type": "boolean" }, "authEnabled": { "type": "boolean" }, "user": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "avatar": { "type": "string" }, "isAdmin": { "type": "boolean" }, "provider": { "type": "string" } } } }, "required": [ "authenticated", "authEnabled" ] }, "example": { "authenticated": true, "authEnabled": true, "user": { "id": 1, "username": "admin", "email": "admin@example.com", "displayName": "Admin", "avatar": null, "isAdmin": true, "provider": "local" } } } } }, "500": { "description": "Unexpected error while validating the session" } }, "security": [] } }, "/api/auth/settings": { "get": { "operationId": "get_api_auth_settings", "tags": [ "auth" ], "summary": "Get the global authentication settings (whether auth is enabled and the default provider)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "authEnabled": { "type": "boolean" }, "defaultProvider": { "type": "string" } }, "required": [ "authEnabled" ] }, "example": { "authEnabled": true, "defaultProvider": "local" } } } }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing settings:view)" }, "500": { "description": "Failed to read the auth settings" } }, "security": [] }, "put": { "operationId": "put_api_auth_settings", "tags": [ "auth" ], "summary": "Update the global authentication settings (enabling auth requires at least one admin/user to exist)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "authEnabled": { "type": "boolean" }, "defaultProvider": { "type": "string" } }, "required": [ "authEnabled" ] }, "example": { "authEnabled": true, "defaultProvider": "string" } } } }, "400": { "description": "Cannot enable authentication without an existing user/admin (response includes requiresUser:true)" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing settings:edit)" }, "500": { "description": "Failed to update the auth settings" } }, "security": [], "description": "sessionTimeout is in seconds (clamped 3600..604800); 0 disables expiry so sessions never time out.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "authEnabled": { "type": "boolean" }, "defaultProvider": { "type": "string" }, "sessionTimeout": { "type": "integer" } } }, "example": { "authEnabled": true, "defaultProvider": "local", "sessionTimeout": 86400 } } } } } }, "/api/auth/tokens": { "get": { "operationId": "get_api_auth_tokens", "tags": [ "auth" ], "summary": "List the authenticated user's API tokens (never returns the hash/secret)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "tokenPrefix": { "type": "string" }, "lastUsed": { "type": "string" }, "expiresAt": { "type": "string" }, "createdAt": { "type": "string" } }, "required": [ "id", "name", "tokenPrefix", "createdAt" ] } }, "example": [ { "id": 1, "name": "CI/CD Pipeline", "tokenPrefix": "a1b2c3d4", "lastUsed": "2027-01-02T08:00:00Z", "expiresAt": "2027-01-01T00:00:00Z", "createdAt": "2026-06-01T10:00:00Z" } ] } } }, "400": { "description": "Authentication is not enabled" }, "401": { "description": "Not authenticated" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_auth_tokens", "tags": [ "auth" ], "summary": "Create a new API token for the authenticated user (requires a session, not a Bearer token)", "parameters": [], "responses": { "201": { "description": "Token created — the plaintext value is shown ONLY this once and cannot be retrieved again", "content": { "application/json": { "schema": { "type": "object", "properties": { "token": { "type": "string" }, "id": { "type": "integer" }, "tokenPrefix": { "type": "string" } }, "required": [ "token", "id", "tokenPrefix" ] }, "example": { "token": "dh_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "id": 42, "tokenPrefix": "a1b2c3d4" } } } }, "400": { "description": "Invalid body, missing password for local users, or MAX_TOKENS_PER_USER (25) reached" }, "401": { "description": "Not authenticated" }, "403": { "description": "Token creation attempted via Bearer auth (session login required) or wrong password" }, "404": { "description": "User record not found while confirming the password (local provider)" }, "429": { "description": "Too many failed password confirmation attempts (rate limited)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "expiresAt": { "type": "string" }, "password": { "type": "string" } }, "required": [ "name" ] }, "example": { "name": "CI/CD Pipeline", "expiresAt": "2027-01-01T00:00:00Z", "password": "correct horse battery staple" } } } } } }, "/api/auth/tokens/{id}": { "delete": { "operationId": "delete_api_auth_tokens_id", "tags": [ "auth" ], "summary": "Revoke (permanently delete) one of the authenticated user's API tokens", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Token id (from GET /api/auth/tokens)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Authentication not enabled, or id is not a valid integer" }, "401": { "description": "Not authenticated" }, "403": { "description": "Attempted via Bearer auth (a leaked token cannot revoke other tokens — session required)" }, "404": { "description": "Token not found, or belongs to a different (non-admin) user" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/auto-update": { "get": { "operationId": "get_api_auto-update", "tags": [ "auto-update" ], "summary": "Get all enabled auto-update settings for an environment, keyed by container name", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID to read settings for (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": {} }, "example": { "web-1": { "enabled": true, "scheduleType": "daily", "cronExpression": "0 3 * * *", "vulnerabilityCriteria": "never" } } } } }, "500": { "description": "Failed to get auto-update settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/auto-update/{containerName}": { "get": { "operationId": "get_api_auto-update_containerName", "tags": [ "auto-update" ], "summary": "Get the auto-update setting for a container (returns sensible defaults when none is stored)", "parameters": [ { "name": "containerName", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container name (URL-encoded)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the container belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "scheduleType": { "type": "string" }, "cronExpression": { "type": "string" }, "vulnerabilityCriteria": { "type": "string" } }, "required": [ "enabled", "scheduleType", "vulnerabilityCriteria" ] }, "example": { "enabled": false, "scheduleType": "daily", "cronExpression": "0 3 * * *", "vulnerabilityCriteria": "never" } } } }, "500": { "description": "Failed to get auto-update setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_auto-update_containerName", "tags": [ "auto-update" ], "summary": "Create or update a container's auto-update setting; enabled=false deletes it and its schedule", "parameters": [ { "name": "containerName", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container name (URL-encoded)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the container belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "enabled": { "type": "boolean" }, "scheduleType": { "type": "string" }, "cronExpression": { "type": "string" }, "vulnerabilityCriteria": { "type": "string" }, "deleted": { "type": "boolean" }, "success": { "type": "boolean" } }, "required": [ "id", "enabled", "scheduleType", "vulnerabilityCriteria" ] }, "example": { "success": true, "deleted": true } } } }, "500": { "description": "Failed to save auto-update setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "cronExpression": { "type": "string" }, "cron_expression": { "type": "string" }, "vulnerabilityCriteria": { "type": "string" }, "vulnerability_criteria": { "type": "string" } } }, "example": { "enabled": true, "cronExpression": "0 3 * * *", "vulnerabilityCriteria": "never" } } } } }, "delete": { "operationId": "delete_api_auto-update_containerName", "tags": [ "auto-update" ], "summary": "Delete a container's auto-update setting and unregister its schedule", "parameters": [ { "name": "containerName", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container name (URL-encoded)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the container belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "500": { "description": "Failed to delete auto-update setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/backup/configs": { "get": { "operationId": "get_api_backup_configs", "tags": [ "backup" ], "summary": "List backup configurations, optionally filtered by type, target and environment (enterprise callers only see configs for environments they can access)", "parameters": [ { "name": "type", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by backup config type (e.g. \"container\", \"stack\")" }, { "name": "target", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by the backed-up target name (container or stack name)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Array of backup configuration objects" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:view\") is produced by the shared requireBackups route guard." }, "post": { "operationId": "post_api_backup_configs", "tags": [ "backup" ], "summary": "Create a backup configuration for a container or stack, optionally scheduled, and register its cron schedule when enabled", "parameters": [], "responses": { "201": { "description": "The created backup configuration object" }, "400": { "description": "Invalid input — missing destinationId/targetName, invalid targetName, invalid cron expression, invalid retention, or a local repository paired with a remote environment / non-backupable stack" }, "403": { "description": "Access denied to the requested environment (enterprise)" }, "500": { "description": "Failed to create the backup configuration (persistence error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard. A requested environmentId the caller can't access also 403s (enterprise). destinationId from GET /api/backup/destinations. environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "destinationId": { "type": "integer" }, "targetName": { "type": "string" }, "type": { "type": "string" }, "environmentId": { "type": "integer" }, "enabled": { "type": "boolean" }, "allVolumes": { "type": "boolean" }, "selectedVolumes": { "type": "array", "items": { "type": "string" } }, "stopBeforeBackup": { "type": "boolean" }, "schedule": { "type": "string" }, "retention": { "type": "object", "properties": { "keepLast": { "type": "integer" }, "keepDaily": { "type": "integer" }, "keepWeekly": { "type": "integer" }, "keepMonthly": { "type": "integer" }, "keepYearly": { "type": "integer" } } }, "options": { "type": "object", "properties": {} }, "tags": { "type": "array", "items": { "type": "string" } } }, "required": [ "destinationId", "targetName" ] }, "example": { "destinationId": 3, "targetName": "nextcloud", "type": "container", "environmentId": 1, "enabled": true, "allVolumes": true, "stopBeforeBackup": false, "schedule": "0 3 * * *", "retention": { "keepDaily": 7, "keepWeekly": 4 } } } } } } }, "/api/backup/configs/{id}": { "get": { "operationId": "get_api_backup_configs_id", "tags": [ "backup" ], "summary": "Fetch a single backup configuration by id, enforcing environment-scoped access", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup configuration id (from GET /api/backup/configs)" } ], "responses": { "200": { "description": "The backup configuration object" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission (\"backups:view\") and environment-access denials (403) and not-found (404) are produced by the shared route guards." }, "put": { "operationId": "put_api_backup_configs_id", "tags": [ "backup" ], "summary": "Update a backup configuration and re-register or remove its cron schedule accordingly", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup configuration id (from GET /api/backup/configs)" } ], "responses": { "200": { "description": "The updated backup configuration object" }, "400": { "description": "Invalid input — invalid cron expression, invalid retention, or a local repository paired with a remote environment" }, "409": { "description": "Cannot change the destination while a backup is running for this config" }, "500": { "description": "Update failed (persistence error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission (\"backups:manage\") and environment-access denials (403) and not-found (404) are produced by the shared route guards. The environment is fixed at creation and cannot be changed here. destinationId from GET /api/backup/destinations.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "destinationId": { "type": "integer" }, "enabled": { "type": "boolean" }, "allVolumes": { "type": "boolean" }, "selectedVolumes": { "type": "array", "items": { "type": "string" } }, "stopBeforeBackup": { "type": "boolean" }, "schedule": { "type": "string" }, "retention": { "type": "object", "properties": { "keepLast": { "type": "integer" }, "keepDaily": { "type": "integer" }, "keepWeekly": { "type": "integer" }, "keepMonthly": { "type": "integer" }, "keepYearly": { "type": "integer" } } }, "options": { "type": "object", "properties": {} }, "tags": { "type": "array", "items": { "type": "string" } } } }, "example": { "schedule": "0 4 * * *", "enabled": true, "stopBeforeBackup": true, "retention": { "keepDaily": 14 } } } } } }, "delete": { "operationId": "delete_api_backup_configs_id", "tags": [ "backup" ], "summary": "Delete a backup configuration and unregister its schedule", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup configuration id (from GET /api/backup/configs)" } ], "responses": { "200": { "description": "Returns { success: true } once the configuration is deleted" }, "409": { "description": "A backup is currently running for this config — stop it before deleting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission (\"backups:manage\") and environment-access denials (403) and not-found (404) are produced by the shared route guards." } }, "/api/backup/configs/{id}/run": { "post": { "operationId": "post_api_backup_configs_id_run", "tags": [ "backup" ], "summary": "Trigger a manual backup run for a configuration, streaming progress as a Server-Sent Events job", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup configuration id (from GET /api/backup/configs)" } ], "responses": { "200": { "description": "Server-Sent Events stream of progress and a final result event (status \"success\", \"warning\", \"skipped\" or \"error\")" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns a text/event-stream that emits `progress` events during the run and a final `result` event. Permission (\"backups:manage\") and environment-access denials (403) and not-found (404) are produced by the shared route guards." } }, "/api/backup/configs/{id}/stop": { "post": { "operationId": "post_api_backup_configs_id_stop", "tags": [ "backup" ], "summary": "Cancel the in-flight backup for a configuration", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup configuration id (from GET /api/backup/configs)" } ], "responses": { "200": { "description": "Returns { success: true, stopped } where \"stopped\" indicates whether a running backup helper was actually killed" }, "500": { "description": "Failed to cancel the backup (internal error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission (\"backups:manage\") and environment-access denials (403) and not-found (404) are produced by the shared route guards." } }, "/api/backup/destinations": { "get": { "operationId": "get_api_backup_destinations", "tags": [ "backup" ], "summary": "List all backup destinations (restic repositories); the password is stripped and cloud-credential env vars are omitted from the list view", "parameters": [], "responses": { "200": { "description": "Array of backup destination objects without secrets (no password, no envVars)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:view\") is produced by the shared requireBackups route guard." }, "post": { "operationId": "post_api_backup_destinations", "tags": [ "backup" ], "summary": "Create a restic backup destination, auto-initialize and test the repository, and register its default maintenance schedules", "parameters": [], "responses": { "201": { "description": "The created backup destination object (includes decrypted envVars since the caller just supplied them; password is stripped)" }, "400": { "description": "Invalid input — missing name/repository/password, unsupported/SSRF-blocked repository, invalid restic flags, or an invalid cron schedule in the policies" }, "409": { "description": "A destination with this name already exists" }, "500": { "description": "Failed to create the destination (persistence error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "repository": { "type": "string" }, "password": { "type": "string" }, "envVars": { "type": "object", "properties": {} }, "flags": { "type": "string" }, "hostPath": { "type": "string" }, "cacert": { "type": "string" }, "tlsClientCert": { "type": "string" }, "policies": { "type": "string" } }, "required": [ "name", "repository", "password" ] }, "example": { "name": "S3 Offsite", "repository": "s3:s3.amazonaws.com/my-bucket/restic", "password": "***", "envVars": { "AWS_ACCESS_KEY_ID": "***", "AWS_SECRET_ACCESS_KEY": "***" } } } } } } }, "/api/backup/destinations/{id}": { "get": { "operationId": "get_api_backup_destinations_id", "tags": [ "backup" ], "summary": "Fetch a single backup destination; decrypted cloud-credential env vars are only included for callers who can manage backups, and the password is always stripped", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "The backup destination object (envVars included only for \"backups:manage\" callers; password always stripped)" }, "400": { "description": "Invalid id (not a number)" }, "404": { "description": "Destination not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:view\") is produced by the shared requireBackups route guard." }, "put": { "operationId": "put_api_backup_destinations_id", "tags": [ "backup" ], "summary": "Update a backup destination, re-validating repository and flags when supplied and re-registering maintenance schedules when policies change", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "The updated backup destination object (password stripped, envVars echoed back to the managing caller)" }, "400": { "description": "Invalid input — invalid id, unsupported/SSRF-blocked repository, invalid restic flags, invalid policy cron, or switching to a local repository used by a remote-environment config" }, "404": { "description": "Destination not found" }, "409": { "description": "A backup using this destination is currently running, or a destination with the new name already exists" }, "500": { "description": "Update failed (persistence error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "repository": { "type": "string" }, "password": { "type": "string" }, "envVars": { "type": "object", "properties": {} }, "flags": { "type": "string" }, "hostPath": { "type": "string" }, "cacert": { "type": "string" }, "tlsClientCert": { "type": "string" }, "policies": { "type": "string" } } }, "example": { "name": "S3 Offsite (renamed)", "policies": "{\"pruneEnabled\":true,\"pruneSchedule\":\"0 0 1 * *\"}" } } } } }, "delete": { "operationId": "delete_api_backup_destinations_id", "tags": [ "backup" ], "summary": "Delete a backup destination and unregister all its maintenance and dependent backup-config schedules", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "Returns { success: true } once the destination is deleted" }, "400": { "description": "Invalid id (not a number)" }, "404": { "description": "Destination not found" }, "409": { "description": "A backup using this destination is currently running — try again once it finishes" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard." } }, "/api/backup/destinations/{id}/init": { "post": { "operationId": "post_api_backup_destinations_id_init", "tags": [ "backup" ], "summary": "Initialize the restic repository for a destination and record the resulting test status", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "Returns { success: true, message } when the repository is initialized" }, "400": { "description": "Invalid id (not a number)" }, "404": { "description": "Destination not found" }, "500": { "description": "Repository initialization failed; returns { success: false, error }" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard." } }, "/api/backup/destinations/{id}/rotate-key": { "post": { "operationId": "post_api_backup_destinations_id_rotate-key", "tags": [ "backup" ], "summary": "Rotate the restic repository password for a destination and persist the new password", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "Returns { success: true } once the password is rotated and the database is updated" }, "400": { "description": "Invalid input — invalid id, missing passwords, or the current password is incorrect" }, "404": { "description": "Destination not found" }, "409": { "description": "restic rotated the key but the database write failed (manual recovery needed; the response includes dbOutOfSync:true)" }, "500": { "description": "restic call failed for an unrelated reason" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "currentPassword": { "type": "string" }, "newPassword": { "type": "string" } }, "required": [ "currentPassword", "newPassword" ] }, "example": { "currentPassword": "***", "newPassword": "***" } } } } } }, "/api/backup/destinations/{id}/task": { "post": { "operationId": "post_api_backup_destinations_id_task", "tags": [ "backup" ], "summary": "Run a restic repository maintenance task (unlock, check, prune, stats, repair-index or repair-snapshots) against a destination", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "The task result object — { success: true, ... } on success, or { success: false, error } when the task itself fails" }, "400": { "description": "Invalid input — invalid destination id, or an unknown task name (must be one of unlock, check, prune, stats, repair-index, repair-snapshots)" }, "403": { "description": "No access to an environment whose configs use this destination (enterprise)" }, "404": { "description": "Destination not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard. Streamed via createJobResponse — a task failure never surfaces as an HTTP 500; it comes back as a 200 with a failed job result (JSON-preferring callers) or a job/SSE 'result' event with success:false (streaming callers).", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "task": { "type": "string" } }, "required": [ "task" ] }, "example": { "task": "check" } } } } } }, "/api/backup/destinations/{id}/test": { "post": { "operationId": "post_api_backup_destinations_id_test", "tags": [ "backup" ], "summary": "Test connectivity to a saved backup destination's repository and update its stored test status", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "Test result — { success: true, status: \"success\" } when reachable, or { success: false, status: \"needs_init\" | \"failed\", error } otherwise" }, "400": { "description": "Invalid id (not a number)" }, "404": { "description": "Destination not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard." } }, "/api/backup/destinations/{id}/verify": { "post": { "operationId": "post_api_backup_destinations_id_verify", "tags": [ "backup" ], "summary": "Verify the integrity of a destination's backups by reading a data subset, streaming progress as a Server-Sent Events job", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Backup destination id (from GET /api/backup/destinations)" } ], "responses": { "200": { "description": "Server-Sent Events stream of progress and a final result event ({ success, ... })" }, "400": { "description": "Invalid destination id (not a number)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns a text/event-stream that emits `progress` events and a final `result` event. Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "dataSubset": { "type": "string" } } }, "example": { "dataSubset": "5%" } } } } } }, "/api/backup/destinations/test": { "post": { "operationId": "post_api_backup_destinations_test", "tags": [ "backup" ], "summary": "Test a backup repository — either a saved destination (by destinationId) or inline credentials supplied for a not-yet-saved destination", "parameters": [], "responses": { "200": { "description": "Test result — { success: true } when the repository is reachable, or { success: false, needsInit?, error } otherwise" }, "400": { "description": "Invalid input — missing repository/password for an inline test, or an unsupported/SSRF-blocked repository" }, "404": { "description": "Destination not found (when testing a saved destinationId)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Provide `destinationId` to test a saved destination using its stored credentials, or omit it and supply `repository`/`password`/`envVars` to test before saving. Permission denial (403, \"backups:manage\") is produced by the shared requireBackups route guard. destinationId from GET /api/backup/destinations.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "destinationId": { "type": "integer" }, "repository": { "type": "string" }, "password": { "type": "string" }, "envVars": { "type": "object", "properties": {} }, "cacert": { "type": "string" }, "tlsClientCert": { "type": "string" } } }, "example": { "repository": "s3:s3.amazonaws.com/my-bucket/restic", "password": "***", "envVars": { "AWS_ACCESS_KEY_ID": "***", "AWS_SECRET_ACCESS_KEY": "***" } } } } } } }, "/api/backup/instance": { "get": { "operationId": "get_api_backup_instance", "tags": [ "backup" ], "summary": "This install's stable backup instance id, used to tell own snapshots from foreign orphans", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "instanceId": { "type": "string" } }, "required": [ "instanceId" ] }, "example": { "instanceId": "string" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Requires backups:view; 403 (no permission) and 404 (backup feature disabled) come from the shared requireBackups guard." } }, "/api/backup/restore": { "post": { "operationId": "post_api_backup_restore", "tags": [ "backup" ], "summary": "Restore a backup snapshot in-place or to a new location, streaming progress as a Server-Sent Events job", "parameters": [], "responses": { "200": { "description": "Server-Sent Events stream of progress and a final result event (status \"success\" or \"error\")" }, "400": { "description": "Invalid restore request; the response \"issues\" array lists the validation problems" }, "403": { "description": "Permission denied — requires \"backups:manage\", or no access to the target or snapshot-owning environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns a text/event-stream that emits `progress` events and a final `result` event. An in-place restore is destructive and requires confirmOverwrite:true. Authorization and snapshot-environment access are checked before the request shape is validated. destinationId from GET /api/backup/destinations. snapshotId from GET /api/backup/snapshots. environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "destinationId": { "type": "integer" }, "snapshotId": { "type": "string" }, "mode": { "type": "string" }, "targetType": { "type": "string" }, "volumes": { "type": "array", "items": { "type": "string" } }, "environmentId": { "type": "integer" }, "confirmOverwrite": { "type": "boolean" }, "targetPath": { "type": "string" }, "targetName": { "type": "string" }, "postRestore": { "type": "string" }, "volumeDestinations": { "type": "array", "items": { "type": "object", "properties": {} } } }, "required": [ "destinationId", "snapshotId" ] }, "example": { "destinationId": 3, "snapshotId": "a1b2c3d4", "mode": "new-location", "targetType": "container", "targetName": "nextcloud-restored", "environmentId": 1, "confirmOverwrite": false } } } } } }, "/api/backup/restore/preview": { "post": { "operationId": "post_api_backup_restore_preview", "tags": [ "backup" ], "summary": "Preview what a restore would write (resolved targets + volume/stack contents)", "parameters": [], "responses": { "200": { "description": "The snapshot preview, optionally with a resolved targets list" }, "400": { "description": "Missing required fields (destinationId, snapshotId)" }, "403": { "description": "Permission denied (needs backups:view) or access denied to the target environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "With includeTargets it also resolves the concrete restore targets (host paths / volumes); without it, returns the metadata-only preview the initial modal load relies on.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "destinationId": { "type": "integer" }, "snapshotId": { "type": "string" }, "includeTargets": { "type": "boolean" }, "targetEnvId": { "type": "integer" } }, "required": [ "destinationId", "snapshotId" ] }, "example": { "destinationId": 0, "snapshotId": "string", "includeTargets": true, "targetEnvId": 0 } } } } } }, "/api/backup/restore/stop": { "post": { "operationId": "post_api_backup_restore_stop", "tags": [ "backup" ], "summary": "Cancel a running restore — kills the restore helper for a given snapshotId, or all restore helpers when snapshotId is omitted", "parameters": [], "responses": { "200": { "description": "Returns { success: true, stopped } where \"stopped\" indicates whether a running restore helper was actually killed" }, "400": { "description": "Invalid snapshotId" }, "403": { "description": "Permission denied — requires \"backups:manage\", or no access to the targeted environment" }, "500": { "description": "Failed to cancel the restore (internal error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "snapshotId from GET /api/backup/snapshots. environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "snapshotId": { "type": "string" }, "environmentId": { "type": "integer" } } }, "example": { "snapshotId": "a1b2c3d4" } } } } } }, "/api/backup/snapshots": { "get": { "operationId": "get_api_backup_snapshots", "tags": [ "backup" ], "summary": "List restic snapshots for a backup config or a single destination (job-polled)", "parameters": [ { "name": "configId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Backup config id (mutually exclusive with destinationId)" }, { "name": "destinationId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Single destination id (mutually exclusive with configId)" }, { "name": "allDestinations", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "With configId, include every destination of the config" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Invalid or missing configId/destinationId" }, "403": { "description": "Permission denied (needs backups:view) or environment access denied" }, "404": { "description": "Backup config not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Pass configId to list across the config's destinations, or destinationId for one. Job-polled so a reverse proxy can't abort the sync restic snapshots read at ~15s; match each snapshot's instance tag against GET /api/backup/instance to tell own snapshots from foreign orphans." } }, "/api/backup/snapshots/{id}": { "delete": { "operationId": "delete_api_backup_snapshots_id", "tags": [ "backup" ], "summary": "Forget and prune a single snapshot from a destination (destructive restic forget --prune), with a server-authoritative environment access gate", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Restic snapshot id to forget (from GET /api/backup/snapshots)" }, { "name": "destinationId", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "Destination holding the snapshot (required) (from GET /api/backup/destinations)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Optional environment id for an early enterprise access check; the authoritative gate resolves the snapshot's owning environment server-side (from GET /api/environments)" } ], "responses": { "200": { "description": "Returns { success: true } once the snapshot is forgotten and pruned" }, "400": { "description": "Missing/invalid destinationId or an invalid snapshot id" }, "403": { "description": "Permission denied — requires \"backups:manage\", or no access to the snapshot's owning environment" }, "404": { "description": "Snapshot not found for this installation (not owned)" }, "500": { "description": "Failed to forget the snapshot (restic error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/backup/snapshots/{id}/browse": { "get": { "operationId": "get_api_backup_snapshots_id_browse", "tags": [ "backup" ], "summary": "List directory entries at a path inside a snapshot (job-polled)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The restic snapshot id" }, { "name": "destinationId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Destination the snapshot lives in" }, { "name": "path", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Directory path inside the snapshot to list" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment context for access checks" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Missing/invalid destinationId" }, "403": { "description": "Permission denied (needs backups:view) or environment access denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Job-polled so a proxy can't abort the restic read at ~15s." } }, "/api/backup/snapshots/{id}/dump": { "get": { "operationId": "get_api_backup_snapshots_id_dump", "tags": [ "backup" ], "summary": "Read a single file (or archive) out of a snapshot (job-polled restic dump)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The restic snapshot id" }, { "name": "destinationId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Destination the snapshot lives in" }, { "name": "path", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Path inside the snapshot (must resolve under /volumes or /metadata)" }, { "name": "type", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Payload kind (e.g. inline preview vs archive)" }, { "name": "download", "in": "query", "required": false, "schema": { "type": "string" }, "description": "When set, stream the file/directory as a tar" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Missing/invalid destinationId or path" }, "403": { "description": "Permission denied (needs backups:view), or a raw metadata.json download was refused" }, "500": { "description": "The restic dump failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Job-polled so a proxy can't abort the restic dump at ~15s. type selects the payload (inline preview vs archive); download requests a raw file/directory tar. A raw metadata.json download is refused (403) - it would bypass redaction." } }, "/api/backup/snapshots/{id}/metadata": { "get": { "operationId": "get_api_backup_snapshots_id_metadata", "tags": [ "backup" ], "summary": "The snapshot's redacted metadata layout (job-polled restic dump)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The restic snapshot id" }, { "name": "destinationId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Destination the snapshot lives in" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Missing/invalid destinationId" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Job-polled so a proxy can't abort the restic dump at ~15s. The layout is redacted (stack secrets and container Config.Env/Labels stripped) before it leaves the server." } }, "/api/backup/snapshots/diff": { "get": { "operationId": "get_api_backup_snapshots_diff", "tags": [ "backup" ], "summary": "Diff two snapshots' file trees (added/changed/removed) - job-polled", "parameters": [ { "name": "destinationId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Destination both snapshots live in" }, { "name": "snapshotA", "in": "query", "required": false, "schema": { "type": "string" }, "description": "The baseline snapshot id" }, { "name": "snapshotB", "in": "query", "required": false, "schema": { "type": "string" }, "description": "The snapshot to compare against the baseline" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Missing destinationId, snapshotA, or snapshotB" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Job-polled so a proxy can't abort the restic read at ~15s." } }, "/api/backup/stack-dir-listing": { "get": { "operationId": "get_api_backup_stack-dir-listing", "tags": [ "backup" ], "summary": "Probe the target host and list the actual contents of a stack's directory, for the backup create/edit dialog's file picker", "parameters": [ { "name": "target", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Target stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "The stack directory probe result — kind \"listed\"/\"tar\" with hostPath/localStackDir and entries, or kind \"helper-failed\"/\"unknown\" with a reason (never throws for an operational failure)" }, "400": { "description": "target is required" }, "403": { "description": "Permission denied (requires backups:view), or no access to the target's environment (enterprise)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/backup/stack-path": { "get": { "operationId": "get_api_backup_stack-path", "tags": [ "backup" ], "summary": "Preview where a stack's directory will be captured from on the host, without running a backup", "parameters": [ { "name": "target", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Target stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "The resolved capture path preview — kind \"candidate\" with hostPath, kind \"tar\" with composeFile only, or kind \"unknown\" with a reason" }, "400": { "description": "target is required" }, "403": { "description": "Permission denied (requires backups:view), or no access to the target's environment (enterprise)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/batch": { "post": { "operationId": "post_api_batch", "tags": [ "batch" ], "summary": "Run a bulk operation (start/stop/restart/pause/unpause/remove/down, depending on entity type) across many containers, images, volumes, networks, or stacks; clients that send Accept application/json get a synchronous summary, others get a jobId and progress over Server-Sent Events", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the entities belong to (from GET /api/environments)" } ], "responses": { "200": { "description": "Either a synchronous completion summary ({type:\"complete\", summary:{total, success, failed}}) or an async job handle ({jobId}) whose progress streams over SSE" }, "400": { "description": "Invalid JSON body, invalid entity type, invalid operation for the entity type, or empty items array" }, "403": { "description": "Permission denied for the requested operation, or environment access denied on enterprise" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "operation": { "type": "string" }, "entityType": { "type": "string" }, "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name" ] } }, "options": { "type": "object", "properties": { "force": { "type": "boolean" }, "removeVolumes": { "type": "boolean" } } } }, "required": [ "operation", "entityType", "items" ] }, "example": { "operation": "restart", "entityType": "containers", "items": [ { "id": "abc123", "name": "web" } ], "options": { "force": false } } } } } } }, "/api/changelog": { "get": { "operationId": "get_api_changelog", "tags": [ "changelog" ], "summary": "Return the bundled Dockhand changelog data", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "version": { "type": "string" }, "date": { "type": "string" }, "changes": { "type": "array", "items": { "type": "string" } } }, "required": [ "version" ] } }, "example": [ { "version": "1.0.39", "date": "2026-06-01", "changes": [ "Fixed image export streaming" ] } ] } } } }, "security": [] } }, "/api/config-sets": { "get": { "operationId": "get_api_config-sets", "tags": [ "config-sets" ], "summary": "List all config sets (reusable bundles of env vars, labels, ports, volumes, and runtime defaults)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "labels": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "ports": { "type": "array", "items": { "type": "object", "properties": { "hostPort": { "type": "string" }, "containerPort": { "type": "string" }, "protocol": { "type": "string" } }, "required": [ "hostPort", "containerPort", "protocol" ] } }, "volumes": { "type": "array", "items": { "type": "object", "properties": { "hostPath": { "type": "string" }, "containerPath": { "type": "string" }, "mode": { "type": "string" } }, "required": [ "hostPath", "containerPath", "mode" ] } }, "networkMode": { "type": "string" }, "restartPolicy": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "networkMode", "restartPolicy", "createdAt", "updatedAt" ] } }, "example": [ { "id": 1, "name": "web-defaults", "description": "Defaults for web stacks", "envVars": [ { "key": "TZ", "value": "UTC" } ], "labels": [], "ports": [], "volumes": [], "networkMode": "bridge", "restartPolicy": "unless-stopped", "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-01T10:00:00Z" } ] } } }, "403": { "description": "Permission denied (requires configsets:view)" }, "500": { "description": "Failed to fetch config sets" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_config-sets", "tags": [ "config-sets" ], "summary": "Create a new config set", "parameters": [], "responses": { "201": { "description": "Config set created", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "labels": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "ports": { "type": "array", "items": { "type": "object", "properties": { "hostPort": { "type": "string" }, "containerPort": { "type": "string" }, "protocol": { "type": "string" } }, "required": [ "hostPort", "containerPort", "protocol" ] } }, "volumes": { "type": "array", "items": { "type": "object", "properties": { "hostPath": { "type": "string" }, "containerPath": { "type": "string" }, "mode": { "type": "string" } }, "required": [ "hostPath", "containerPath", "mode" ] } }, "networkMode": { "type": "string" }, "restartPolicy": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "networkMode", "restartPolicy", "createdAt", "updatedAt" ] }, "example": { "id": 1, "name": "web-defaults", "description": "Defaults for web stacks", "envVars": [ { "key": "TZ", "value": "UTC" } ], "labels": [], "ports": [], "volumes": [], "networkMode": "bridge", "restartPolicy": "unless-stopped", "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-01T10:00:00Z" } } } }, "400": { "description": "Name is required, or a config set with this name already exists" }, "403": { "description": "Permission denied (requires configsets:create)" }, "500": { "description": "Failed to create config set" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "labels": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "ports": { "type": "array", "items": { "type": "object", "properties": { "hostPort": { "type": "string" }, "containerPort": { "type": "string" }, "protocol": { "type": "string" } }, "required": [ "hostPort", "containerPort", "protocol" ] } }, "volumes": { "type": "array", "items": { "type": "object", "properties": { "hostPath": { "type": "string" }, "containerPath": { "type": "string" }, "mode": { "type": "string" } }, "required": [ "hostPath", "containerPath", "mode" ] } }, "networkMode": { "type": "string" }, "restartPolicy": { "type": "string" } }, "required": [ "name" ] }, "example": { "name": "web-defaults", "description": "Defaults for web stacks", "envVars": [ { "key": "TZ", "value": "UTC" } ], "networkMode": "bridge", "restartPolicy": "unless-stopped" } } } } } }, "/api/config-sets/{id}": { "get": { "operationId": "get_api_config-sets_id", "tags": [ "config-sets" ], "summary": "Fetch a single config set by its numeric ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Config set ID (from GET /api/config-sets)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "labels": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "ports": { "type": "array", "items": { "type": "object", "properties": { "hostPort": { "type": "string" }, "containerPort": { "type": "string" }, "protocol": { "type": "string" } }, "required": [ "hostPort", "containerPort", "protocol" ] } }, "volumes": { "type": "array", "items": { "type": "object", "properties": { "hostPath": { "type": "string" }, "containerPath": { "type": "string" }, "mode": { "type": "string" } }, "required": [ "hostPath", "containerPath", "mode" ] } }, "networkMode": { "type": "string" }, "restartPolicy": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "networkMode", "restartPolicy", "createdAt", "updatedAt" ] }, "example": { "id": 1, "name": "web-defaults", "description": "Defaults for web stacks", "envVars": [ { "key": "TZ", "value": "UTC" } ], "labels": [], "ports": [], "volumes": [], "networkMode": "bridge", "restartPolicy": "unless-stopped", "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-01T10:00:00Z" } } } }, "400": { "description": "Invalid ID" }, "403": { "description": "Permission denied (requires configsets:view)" }, "404": { "description": "Config set not found" }, "500": { "description": "Failed to fetch config set" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_config-sets_id", "tags": [ "config-sets" ], "summary": "Update an existing config set by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Config set ID (from GET /api/config-sets)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "labels": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "ports": { "type": "array", "items": { "type": "object", "properties": { "hostPort": { "type": "string" }, "containerPort": { "type": "string" }, "protocol": { "type": "string" } }, "required": [ "hostPort", "containerPort", "protocol" ] } }, "volumes": { "type": "array", "items": { "type": "object", "properties": { "hostPath": { "type": "string" }, "containerPath": { "type": "string" }, "mode": { "type": "string" } }, "required": [ "hostPath", "containerPath", "mode" ] } }, "networkMode": { "type": "string" }, "restartPolicy": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "networkMode", "restartPolicy", "createdAt", "updatedAt" ] }, "example": { "id": 1, "name": "web-defaults", "description": "Defaults for web stacks", "envVars": [], "labels": [], "ports": [], "volumes": [], "networkMode": "host", "restartPolicy": "always", "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-02T12:00:00Z" } } } }, "400": { "description": "Invalid ID, or a config set with this name already exists" }, "403": { "description": "Permission denied (requires configsets:edit)" }, "404": { "description": "Config set not found" }, "500": { "description": "Failed to update config set" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "labels": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" } }, "required": [ "key", "value" ] } }, "ports": { "type": "array", "items": { "type": "object", "properties": { "hostPort": { "type": "string" }, "containerPort": { "type": "string" }, "protocol": { "type": "string" } }, "required": [ "hostPort", "containerPort", "protocol" ] } }, "volumes": { "type": "array", "items": { "type": "object", "properties": { "hostPath": { "type": "string" }, "containerPath": { "type": "string" }, "mode": { "type": "string" } }, "required": [ "hostPath", "containerPath", "mode" ] } }, "networkMode": { "type": "string" }, "restartPolicy": { "type": "string" } } }, "example": { "name": "web-defaults", "networkMode": "host", "restartPolicy": "always" } } } } }, "delete": { "operationId": "delete_api_config-sets_id", "tags": [ "config-sets" ], "summary": "Delete a config set by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Config set ID (from GET /api/config-sets)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid ID" }, "403": { "description": "Permission denied (requires configsets:delete)" }, "404": { "description": "Config set not found" }, "500": { "description": "Failed to delete config set" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/container-icons": { "get": { "operationId": "get_api_container-icons", "tags": [ "container-icons" ], "summary": "List all container icon overrides for an environment as a name -> icon map", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id" } ], "responses": { "200": { "description": "A JSON object mapping container name to its icon value (lucide name, `selfhst:`, or `custom:container`)" }, "403": { "description": "Permission denied (needs containers:view)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns every container that has a user-set icon override in the given environment, so the containers list can render them in one request instead of one lookup per row." } }, "/api/container-icons/{name}": { "get": { "operationId": "get_api_container-icons_name", "tags": [ "container-icons" ], "summary": "Get a container's uploaded custom icon (raw image/webp bytes, not JSON)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the container belongs to" } ], "responses": { "200": { "description": "Binary image/webp response body, Cache-Control public max-age=3600" }, "403": { "description": "Permission denied (needs containers:view)" }, "404": { "description": "No custom icon set for this container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_container-icons_name", "tags": [ "container-icons" ], "summary": "Set a container's icon override - either a name reference or an uploaded image", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the container belongs to" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "icon": { "type": "string" } }, "required": [ "success", "icon" ] }, "example": { "success": true, "icon": "selfhst:plex" } } } }, "400": { "description": "Neither icon nor image supplied, or image exceeds the size limit" }, "403": { "description": "Permission denied (needs containers:edit)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Body carries EITHER `icon` (a lucide name or `selfhst:`) to set a referenced icon, OR `image` (a base64-encoded data URL, ~300KB limit) to upload a custom one (stored as `custom:container`). One of the two is required. The override is keyed by container name + environment, so it survives container recreation.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "icon": { "type": "string" }, "image": { "type": "string" } } }, "example": { "icon": "string", "image": "string" } } } } }, "delete": { "operationId": "delete_api_container-icons_name", "tags": [ "container-icons" ], "summary": "Remove a container's icon override (fall back to automatic matching)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the container belongs to" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (needs containers:edit)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers": { "get": { "operationId": "get_api_containers", "tags": [ "containers" ], "summary": "List containers for one environment (dockhand.hidden=true label is filtered out)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id — an empty array is returned if omitted (from GET /api/environments)" }, { "name": "all", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Include stopped containers (default true; pass \"false\" to only show running ones)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "image": { "type": "string" }, "state": { "type": "string" }, "status": { "type": "string" } }, "required": [ "id", "name", "image", "state", "status" ] } }, "example": [ { "id": "a1b2c3d4e5f6", "name": "immich_server", "image": "ghcr.io/immich-app/immich-server:latest", "state": "running", "status": "Up 2 hours (healthy)" } ] } } }, "403": { "description": "Permission denied, or access denied to this environment" }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_containers", "tags": [ "containers" ], "summary": "Create a new container in an environment (auto-pulls the image first if it isn't present locally), optionally starting it right away", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "id": { "type": "string" }, "imagePulled": { "type": "boolean" } }, "required": [ "success", "id" ] }, "example": { "success": true, "id": "string", "imagePulled": true } } } }, "403": { "description": "Permission denied, or access denied to this environment" }, "500": { "description": "Container creation failed, or the image could not be pulled" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "image": { "type": "string" }, "ports": { "type": "object", "properties": {} }, "volumes": { "type": "object", "properties": {} }, "volumeBinds": { "type": "array", "items": { "type": "string" } }, "env": { "type": "array", "items": { "type": "string" } }, "labels": { "type": "object", "properties": {} }, "cmd": { "type": "array", "items": { "type": "string" } }, "entrypoint": { "type": "array", "items": { "type": "string" } }, "workingDir": { "type": "string" }, "restartPolicy": { "type": "string" }, "restartMaxRetries": { "type": "integer" }, "networkMode": { "type": "string" }, "additionalNetworks": { "type": "array", "items": { "type": "string" } }, "networkAliases": { "type": "array", "items": { "type": "string" } }, "networkIpv4Address": { "type": "string" }, "networkIpv6Address": { "type": "string" }, "startAfterCreate": { "type": "boolean" } }, "required": [ "name", "image" ] }, "example": { "name": "my-app", "image": "nginx:latest", "startAfterCreate": true } } } } } }, "/api/containers/{id}": { "get": { "operationId": "get_api_containers_id", "tags": [ "containers" ], "summary": "Return the full Docker inspect payload for a single container", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "The raw Docker inspect object for the container" }, "403": { "description": "Permission denied, or (enterprise) no access to the requested environment" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to inspect the container (e.g. it no longer exists or the daemon is unreachable)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "operationId": "delete_api_containers_id", "tags": [ "containers" ], "summary": "Remove a container and clean up its auto-update schedule and pending-update record", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "force", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Force removal of a running container (Docker force flag)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied, or (enterprise) no access to the requested environment" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to remove the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/compose": { "get": { "operationId": "get_api_containers_id_compose", "tags": [ "containers" ], "summary": "Generate a docker-compose service definition from a running container's inspect (requires the 'view' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "compose keeps only user-set env; composeFullEnv keeps every env var (incl. image-inherited); stackProject is the compose project the container already belongs to, or null", "content": { "application/json": { "schema": { "type": "object", "properties": { "compose": { "type": "string" }, "composeFullEnv": { "type": "string" }, "serviceName": { "type": "string" }, "stackProject": { "type": "string" } } }, "example": { "compose": "string", "composeFullEnv": "string", "serviceName": "string", "stackProject": "string" } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to inspect the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/exec": { "post": { "operationId": "post_api_containers_id_exec", "tags": [ "containers" ], "summary": "Create a Docker exec instance in a container and return its ID plus the Docker connection info for a terminal WebSocket", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "envId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "execId": { "type": "string" }, "connectionInfo": { "type": "object", "properties": { "type": { "type": "string" }, "host": { "type": "string" }, "port": { "type": "integer" } }, "required": [ "type" ] } }, "required": [ "execId" ] }, "example": { "execId": "string", "connectionInfo": { "type": "string", "host": "string", "port": 0 } } } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to create the exec instance" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "shell": { "type": "string" }, "user": { "type": "string" } } }, "example": { "shell": "/bin/sh", "user": "root" } } } } } }, "/api/containers/{id}/files": { "get": { "operationId": "get_api_containers_id_files", "tags": [ "containers" ], "summary": "List the contents of a directory inside a container's filesystem", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "path", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Absolute directory path inside the container (default \"/\")" }, { "name": "simpleLs", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Use a lightweight `ls` listing instead of a full stat of each entry" } ], "responses": { "200": { "description": "Directory listing (entries with name, type, size and permission metadata)" }, "403": { "description": "Permission denied" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to list the directory" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/files/chmod": { "post": { "operationId": "post_api_containers_id_files_chmod", "tags": [ "containers" ], "summary": "Change the mode (permissions) of a file or directory inside a container (requires the 'exec' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "path": { "type": "string" }, "mode": { "type": "string" }, "recursive": { "type": "boolean" } }, "required": [ "success", "path", "mode", "recursive" ] }, "example": { "success": true, "path": "/app/entrypoint.sh", "mode": "755", "recursive": false } } } }, "400": { "description": "Path or mode missing, an invalid chmod mode, or the container is not running" }, "403": { "description": "Permission denied, read-only file system, or operation not permitted" }, "404": { "description": "Path not found" }, "500": { "description": "Failed to change permissions" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" }, "mode": { "type": "string" }, "recursive": { "type": "boolean" } }, "required": [ "path", "mode" ] }, "example": { "path": "/app/entrypoint.sh", "mode": "755", "recursive": false } } } } } }, "/api/containers/{id}/files/content": { "get": { "operationId": "get_api_containers_id_files_content", "tags": [ "containers" ], "summary": "Read the content of a single file inside a container (max 1 MB)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "path", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Absolute file path inside the container" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" }, "path": { "type": "string" } }, "required": [ "content", "path" ] }, "example": { "content": "string", "path": "string" } } } }, "400": { "description": "Path is missing, the target is a directory, or the container is not running" }, "403": { "description": "Permission denied to read the file" }, "404": { "description": "File not found" }, "413": { "description": "File is larger than the 1 MB read limit" }, "500": { "description": "Failed to read the file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_containers_id_files_content", "tags": [ "containers" ], "summary": "Overwrite the content of a file inside a container (max 1 MB, requires the 'exec' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "path", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Absolute file path inside the container" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "path": { "type": "string" } }, "required": [ "success", "path" ] }, "example": { "success": true, "path": "/etc/nginx/nginx.conf" } } } }, "400": { "description": "Path is missing, content is missing/not a string, or the container is not running" }, "403": { "description": "Permission denied, or the target file system is read-only" }, "404": { "description": "Target directory not found" }, "413": { "description": "Content is larger than the 1 MB write limit" }, "500": { "description": "Failed to write the file" }, "507": { "description": "No space left on device" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" } }, "required": [ "content" ] }, "example": { "content": "server {\n listen 80;\n}\n" } } } } } }, "/api/containers/{id}/files/create": { "post": { "operationId": "post_api_containers_id_files_create", "tags": [ "containers" ], "summary": "Create an empty file or a directory inside a container (requires the 'exec' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "path": { "type": "string" }, "type": { "type": "string" } }, "required": [ "success", "path", "type" ] }, "example": { "success": true, "path": "/app/data", "type": "directory" } } } }, "400": { "description": "Path missing, type not \"file\" or \"directory\", or the container is not running" }, "403": { "description": "Permission denied" }, "404": { "description": "Parent directory not found" }, "409": { "description": "Path already exists" }, "500": { "description": "Failed to create the path" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" }, "type": { "type": "string" } }, "required": [ "path", "type" ] }, "example": { "path": "/app/data", "type": "directory" } } } } } }, "/api/containers/{id}/files/delete": { "delete": { "operationId": "delete_api_containers_id_files_delete", "tags": [ "containers" ], "summary": "Delete a file or directory inside a container (requires the 'exec' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "path", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Absolute path inside the container to delete" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "path": { "type": "string" } }, "required": [ "success", "path" ] }, "example": { "success": true, "path": "/app/tmp/old.log" } } } }, "400": { "description": "Path missing, a refused critical-path delete, a non-empty directory, or the container is not running" }, "403": { "description": "Permission denied, or read-only file system" }, "404": { "description": "Path not found" }, "500": { "description": "Failed to delete the path" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/files/download": { "get": { "operationId": "get_api_containers_id_files_download", "tags": [ "containers" ], "summary": "Download a file or directory from a container as a tar (optionally gzip-compressed) archive attachment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "path", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Absolute path inside the container to archive and download" }, { "name": "format", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Archive format, \"tar\" (default) or \"tar.gz\"" } ], "responses": { "200": { "description": "Archive stream (application/x-tar or application/gzip) as a file attachment" }, "400": { "description": "Path is missing" }, "403": { "description": "Permission denied to access the path" }, "404": { "description": "File not found" }, "500": { "description": "Failed to download the file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/files/rename": { "post": { "operationId": "post_api_containers_id_files_rename", "tags": [ "containers" ], "summary": "Rename or move a file or directory inside a container (requires the 'exec' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "oldPath": { "type": "string" }, "newPath": { "type": "string" } }, "required": [ "success", "oldPath", "newPath" ] }, "example": { "success": true, "oldPath": "/app/config.yaml", "newPath": "/app/config.old.yaml" } } } }, "400": { "description": "oldPath or newPath missing, or the container is not running" }, "403": { "description": "Permission denied, or read-only file system" }, "404": { "description": "Source path not found" }, "409": { "description": "Destination already exists" }, "500": { "description": "Failed to rename the path" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "oldPath": { "type": "string" }, "newPath": { "type": "string" } }, "required": [ "oldPath", "newPath" ] }, "example": { "oldPath": "/app/config.yaml", "newPath": "/app/config.old.yaml" } } } } } }, "/api/containers/{id}/files/upload": { "post": { "operationId": "post_api_containers_id_files_upload", "tags": [ "containers" ], "summary": "Upload one or more files into a container directory (requires the 'exec' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "path", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Absolute target directory inside the container" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "uploaded": { "type": "array", "items": { "type": "string" } }, "errors": { "type": "array", "items": { "type": "string" } } }, "required": [ "success", "uploaded" ] }, "example": { "success": true, "uploaded": [ "app.conf" ] } } } }, "400": { "description": "Target path missing, or no files provided" }, "403": { "description": "Permission denied to write to the path" }, "404": { "description": "Target directory not found" }, "500": { "description": "Failed to upload the files" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Send `multipart/form-data` with one or more `files` parts; each file is tar-wrapped and written into the target directory. Partial success is reported per file in `uploaded`/`errors`.", "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "files": { "type": "array", "items": { "type": "string", "format": "binary" }, "description": "One or more files to write into the target directory" } }, "required": [ "files" ] } } } } } }, "/api/containers/{id}/inspect": { "get": { "operationId": "get_api_containers_id_inspect", "tags": [ "containers" ], "summary": "Return the full Docker inspect payload for a container (requires the 'inspect' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "The raw Docker inspect object for the container" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to inspect the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/logs": { "get": { "operationId": "get_api_containers_id_logs", "tags": [ "containers" ], "summary": "Return the last N lines of a container's combined stdout/stderr logs as a single string", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "tail", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Number of trailing log lines to return (default 100)" }, { "name": "since", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only return logs since this time (Unix timestamp or Docker duration, e.g. 10m)" }, { "name": "until", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only return logs before this time (Unix timestamp or Docker duration)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "logs": { "type": "string" } }, "required": [ "logs" ] }, "example": { "logs": "string" } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to read the container logs" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/logs/stream": { "get": { "operationId": "get_api_containers_id_logs_stream", "tags": [ "containers" ], "summary": "Stream a container's logs live as Server-Sent Events (connected/log/end/error events, with keepalive heartbeats)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "tail", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Number of trailing log lines to prime the stream with (default \"100\")" }, { "name": "since", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only stream logs since this time (Unix timestamp or Docker duration, e.g. 10m)" }, { "name": "until", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only stream logs before this time (Unix timestamp or Docker duration)" } ], "responses": { "200": { "description": "Server-Sent Events stream (text/event-stream) of log lines" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns a `text/event-stream`. For Hawser Edge environments whose agent is not connected the stream body carries an error event. Docker stdout/stderr frames are demultiplexed server-side; TTY containers stream raw text." } }, "/api/containers/{id}/pause": { "post": { "operationId": "post_api_containers_id_pause", "tags": [ "containers" ], "summary": "Pause all processes in a running container (requires the container 'stop' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to pause the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/rename": { "post": { "operationId": "post_api_containers_id_rename", "tags": [ "containers" ], "summary": "Rename a container and update any associated auto-update schedule (requires the 'create' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The new name is missing or not a string" }, "403": { "description": "Permission denied" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to rename the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" } }, "required": [ "name" ] }, "example": { "name": "my-renamed-container" } } } } } }, "/api/containers/{id}/restart": { "post": { "operationId": "post_api_containers_id_restart", "tags": [ "containers" ], "summary": "Restart a container", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied, or (enterprise) no access to the requested environment" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to restart the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/shells": { "get": { "operationId": "get_api_containers_id_shells", "tags": [ "containers" ], "summary": "Probe a container for available shells (bash/sh/zsh/ash) and report the preferred default (requires the 'exec' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "shells": { "type": "array", "items": { "type": "string" } }, "defaultShell": { "type": "string" }, "allShells": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "label": { "type": "string" }, "available": { "type": "boolean" } }, "required": [ "path", "label", "available" ] } } }, "required": [ "shells", "allShells" ] }, "example": { "shells": [ "/bin/sh" ], "defaultShell": "/bin/sh", "allShells": [ { "path": "/bin/bash", "label": "Bash", "available": false }, { "path": "/bin/sh", "label": "Shell (sh)", "available": true } ] } } } }, "403": { "description": "Permission denied, or (enterprise) no access to the requested environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "On probe failure the endpoint still returns 200 with empty results rather than an error, so callers always get a usable shell list." } }, "/api/containers/{id}/start": { "post": { "operationId": "post_api_containers_id_start", "tags": [ "containers" ], "summary": "Start a stopped container", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied, or (enterprise) no access to the requested environment" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to start the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/stats": { "get": { "operationId": "get_api_containers_id_stats", "tags": [ "containers" ], "summary": "Return a one-shot CPU/memory/network/block-IO stats snapshot for a container (Docker-CLI-equivalent memory accounting)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "cpuPercent": { "type": "number" }, "memoryUsage": { "type": "integer" }, "memoryRaw": { "type": "integer" }, "memoryCache": { "type": "integer" }, "memoryLimit": { "type": "integer" }, "memoryPercent": { "type": "number" }, "networkRx": { "type": "integer" }, "networkTx": { "type": "integer" }, "blockRead": { "type": "integer" }, "blockWrite": { "type": "integer" }, "timestamp": { "type": "integer" } }, "required": [ "cpuPercent", "memoryUsage", "memoryRaw", "memoryCache", "memoryLimit", "memoryPercent", "networkRx", "networkTx", "blockRead", "blockWrite", "timestamp" ] }, "example": { "cpuPercent": 0, "memoryUsage": 0, "memoryRaw": 0, "memoryCache": 0, "memoryLimit": 0, "memoryPercent": 0, "networkRx": 0, "networkTx": 0, "blockRead": 0, "blockWrite": 0, "timestamp": 0 } } } }, "403": { "description": "Permission denied" }, "404": { "description": "No environment configured, the environment was not found, or the container was not found" }, "500": { "description": "Failed to read the container stats" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/stop": { "post": { "operationId": "post_api_containers_id_stop", "tags": [ "containers" ], "summary": "Stop a running container", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied, or (enterprise) no access to the requested environment" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to stop the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/top": { "get": { "operationId": "get_api_containers_id_top", "tags": [ "containers" ], "summary": "List the processes running inside a container (via `ps` in the container, falling back to the Docker top API)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "Titles": { "type": "array", "items": { "type": "string" } }, "Processes": { "type": "array", "items": { "type": "array", "items": { "type": "string" } } }, "source": { "type": "string" } }, "required": [ "Titles", "Processes", "source" ] }, "example": { "Titles": [ "string" ], "Processes": [ [ "string" ] ], "source": "string" } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to read the container process list" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Uses the container 'inspect' permission. The `source` field indicates whether the result came from an in-container `ps` (`\"ps\"`) or the Docker top API fallback (`\"top\"`)." } }, "/api/containers/{id}/unpause": { "post": { "operationId": "post_api_containers_id_unpause", "tags": [ "containers" ], "summary": "Resume all processes in a paused container (requires the container 'start' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to unpause the container" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/{id}/update": { "post": { "operationId": "post_api_containers_id_update", "tags": [ "containers" ], "summary": "Recreate a container with updated create-options (optionally re-pulling the image first), preserving its configuration (requires the 'create' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "id": { "type": "string" } }, "required": [ "success", "id" ] }, "example": { "success": true, "id": "3f4a1c2b9d8e" } } } }, "403": { "description": "Permission denied" }, "404": { "description": "Container not found" }, "500": { "description": "Failed to update the container (e.g. the image pull or recreation failed)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "The body carries the container create-options (image, name, env, ports, volumes, …) alongside the two control flags below; `repullImage` pulls the image before recreation and `startAfterUpdate` starts the new container once created.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "image": { "type": "string" }, "name": { "type": "string" }, "repullImage": { "type": "boolean" }, "startAfterUpdate": { "type": "boolean" } } }, "example": { "image": "nginx:latest", "name": "web", "repullImage": true, "startAfterUpdate": true } } } } } }, "/api/containers/{id}/update-runtime": { "post": { "operationId": "post_api_containers_id_update-runtime", "tags": [ "containers" ], "summary": "Update a running container's restart policy and/or CPU/memory/blkio/pids limits in-place (no recreate)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container id (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "warnings": { "type": "array", "items": { "type": "string" } } }, "required": [ "success" ] }, "example": { "success": true, "warnings": [ "string" ] } } } }, "400": { "description": "Invalid JSON body, or no supported fields were provided" }, "403": { "description": "Permission denied" }, "404": { "description": "Container not found" }, "500": { "description": "Update failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "RestartPolicy": { "type": "object", "properties": {} }, "CpuShares": { "type": "integer" }, "CpuPeriod": { "type": "integer" }, "CpuQuota": { "type": "integer" }, "CpuRealtimePeriod": { "type": "integer" }, "CpuRealtimeRuntime": { "type": "integer" }, "CpusetCpus": { "type": "string" }, "CpusetMems": { "type": "string" }, "NanoCpus": { "type": "integer" }, "Memory": { "type": "integer" }, "MemorySwap": { "type": "integer" }, "MemoryReservation": { "type": "integer" }, "MemorySwappiness": { "type": "integer" }, "KernelMemory": { "type": "integer" }, "BlkioWeight": { "type": "integer" }, "BlkioWeightDevice": { "type": "array", "items": { "type": "string" } }, "BlkioDeviceReadBps": { "type": "array", "items": { "type": "string" } }, "BlkioDeviceWriteBps": { "type": "array", "items": { "type": "string" } }, "BlkioDeviceReadIOps": { "type": "array", "items": { "type": "string" } }, "BlkioDeviceWriteIOps": { "type": "array", "items": { "type": "string" } }, "PidsLimit": { "type": "integer" } } }, "example": { "Memory": 536870912, "PidsLimit": 512 } } } } } }, "/api/containers/{id}/version-notes": { "get": { "operationId": "get_api_containers_id_version-notes", "tags": [ "containers" ], "summary": "Resolve GitHub release notes for the versions a semver update-check surfaced (requires the 'view' permission)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Container ID or name (from GET /api/containers)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "versions", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Comma-separated version tags to fetch notes for (target + skipped), e.g. \"16.3-alpine,16.4-alpine\"" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "changelogUrl": { "type": "string" }, "source": { "type": "string" }, "rateLimited": { "type": "boolean" }, "notes": { "type": "array", "items": { "type": "object", "properties": { "version": { "type": "string" }, "name": { "type": "string" }, "githubTag": { "type": "string" }, "body": { "type": "string" }, "publishedAt": { "type": "string" }, "url": { "type": "string" } }, "required": [ "version", "url" ] } } }, "required": [ "notes" ] }, "example": { "changelogUrl": "https://github.com/go-gitea/gitea/releases", "source": "go-gitea/gitea", "notes": [ { "version": "1.22.0", "name": "v1.22.0", "githubTag": "v1.22.0", "body": "## Changes...", "publishedAt": "2024-05-01T00:00:00Z", "url": "https://github.com/go-gitea/gitea/releases/tag/v1.22.0" } ] } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to resolve release notes" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/batch-update": { "post": { "operationId": "post_api_containers_batch-update", "tags": [ "containers" ], "summary": "Recreate a set of containers with their latest images, preserving all settings (requires the 'create' permission)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "results": { "type": "array", "items": { "type": "object", "properties": { "containerId": { "type": "string" }, "containerName": { "type": "string" }, "success": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "containerId", "containerName", "success" ] } }, "summary": { "type": "object", "properties": { "total": { "type": "integer" }, "success": { "type": "integer" }, "failed": { "type": "integer" } }, "required": [ "total", "success", "failed" ] } }, "required": [ "success", "results", "summary" ] }, "example": { "success": true, "results": [ { "containerId": "string", "containerName": "string", "success": true, "error": "string" } ], "summary": { "total": 0, "success": 0, "failed": 0 } } } } }, "400": { "description": "The containerIds array is missing or empty" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to run the batch update" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Containers are processed sequentially; the response reports per-container success/failure plus a summary. Use the streaming variant for live progress. containerIds from GET /api/containers.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "containerIds": { "type": "array", "items": { "type": "string" } } }, "required": [ "containerIds" ] }, "example": { "containerIds": [ "3f4a1c2b9d8e", "a1b2c3d4e5f6" ] } } } } } }, "/api/containers/batch-update-stream": { "post": { "operationId": "post_api_containers_batch-update-stream", "tags": [ "containers" ], "summary": "Recreate a set of containers with live streaming progress (Server-Sent Events), optionally blocking on vulnerability criteria (requires the 'create' permission)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Server-Sent Events stream (text/event-stream) of per-container update progress" }, "400": { "description": "Invalid JSON body, or the containerIds array is missing or empty" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns a `text/event-stream` reporting per-container progress. `vulnerabilityCriteria` (default \"never\") can block an update when a container's image scan exceeds the configured severity threshold. containerIds from GET /api/containers.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "containerIds": { "type": "array", "items": { "type": "string" } }, "vulnerabilityCriteria": { "type": "string" } }, "required": [ "containerIds" ] }, "example": { "containerIds": [ "3f4a1c2b9d8e" ], "vulnerabilityCriteria": "never" } } } } } }, "/api/containers/check-updates": { "get": { "operationId": "get_api_containers_check-updates", "tags": [ "containers" ], "summary": "Read the cached pending image-update records for an environment (no fresh check; requires the 'view' permission)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "environmentId": { "type": "integer" }, "pendingUpdates": { "type": "array", "items": { "type": "object", "properties": { "containerId": { "type": "string" }, "containerName": { "type": "string" }, "currentImage": { "type": "string" }, "checkedAt": { "type": "string" } }, "required": [ "containerId", "containerName", "currentImage" ] } } }, "required": [ "environmentId", "pendingUpdates" ] }, "example": { "environmentId": 0, "pendingUpdates": [ { "containerId": "string", "containerName": "string", "currentImage": "string", "checkedAt": "string" } ] } } } }, "400": { "description": "Environment ID required" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to get pending updates" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns the containers currently flagged as having a pending image update from the last check. Does not trigger a new check — use POST to run a fresh check." }, "post": { "operationId": "post_api_containers_check-updates", "tags": [ "containers" ], "summary": "Trigger a fresh image-update check across all (non-hidden, non-podman-infra) containers in an environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "text/event-stream job stream (\"progress\" events with {checked,total}, final \"result\" event with {total,updatesFound,results}) — or, with \"Accept: application/json\", the final result as plain JSON" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/pending-updates": { "get": { "operationId": "get_api_containers_pending-updates", "tags": [ "containers" ], "summary": "List the containers in an environment that have a pending image update recorded (requires the 'view' permission)", "parameters": [ { "name": "env", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "The target environment ID (required) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "environmentId": { "type": "integer" }, "pendingUpdates": { "type": "array", "items": { "type": "object", "properties": { "containerId": { "type": "string" }, "containerName": { "type": "string" }, "currentImage": { "type": "string" }, "checkedAt": { "type": "string" }, "hasImageUpdate": { "type": "boolean" }, "newerVersion": { "type": "string" } }, "required": [ "containerId", "containerName", "currentImage", "checkedAt", "hasImageUpdate" ] } } }, "required": [ "environmentId", "pendingUpdates" ] }, "example": { "environmentId": 0, "pendingUpdates": [ { "containerId": "string", "containerName": "string", "currentImage": "string", "checkedAt": "string", "hasImageUpdate": true, "newerVersion": "string" } ] } } } }, "400": { "description": "Environment ID is required" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to get the pending updates" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "operationId": "delete_api_containers_pending-updates", "tags": [ "containers" ], "summary": "Clear pending-update records for an environment, or for a single container when containerId is given", "parameters": [ { "name": "env", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "The target environment ID (required) (from GET /api/environments)" }, { "name": "containerId", "in": "query", "required": false, "schema": { "type": "string" }, "description": "The container to clear; omit to clear ALL pending updates in the environment (from GET /api/containers)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Environment ID is required" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to remove the pending update" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/sizes": { "get": { "operationId": "get_api_containers_sizes", "tags": [ "containers" ], "summary": "List all containers in an environment together with their writable-layer and root-filesystem sizes (requires the 'view' permission)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" } ], "responses": { "200": { "description": "Array of containers with size metadata (SizeRw / SizeRootFs)" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to get container sizes (returns an empty object)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/containers/stats": { "get": { "operationId": "get_api_containers_stats", "tags": [ "containers" ], "summary": "Return a CPU/memory/network/block-IO stats snapshot for every running container in an environment (requires the 'view' permission)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "The target environment ID (omit for the local/default Docker host) (from GET /api/environments)" }, { "name": "debug", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Return raw Docker stats for the single container with this name instead of the aggregate list" } ], "responses": { "200": { "description": "Array of per-container stats snapshots (or, with `debug`, the raw stats for one container)" }, "403": { "description": "Permission denied" }, "404": { "description": "The requested debug container was not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns an empty array when no environment is configured or specified. With `debug=` it returns the raw memory_stats for a single container instead. On internal error it returns an empty array with status 200." } }, "/api/containers/stats/stream": { "get": { "operationId": "get_api_containers_stats_stream", "tags": [ "containers" ], "summary": "Stream live CPU/memory/network/block-IO stats for all running containers in an environment (one snapshot round, then closes)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id — an immediate \"done\" event is sent if omitted or no environments are configured (from GET /api/environments)" } ], "responses": { "200": { "description": "text/event-stream SSE stream (\"stat\" events per container with {id,name,cpuPercent,memoryUsage,memoryRaw,memoryCache,memoryLimit,memoryPercent,networkRx,networkTx,blockRead,blockWrite}, an \"error\" event on environment-not-found, then a final \"done\" event)" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/dashboard/preferences": { "get": { "operationId": "get_api_dashboard_preferences", "tags": [ "dashboard" ], "summary": "Get the current user's dashboard preferences (grid layout, lock state, view mode)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "gridLayout": { "type": "array", "items": { "type": "object", "properties": {} } }, "locked": { "type": "boolean" }, "viewMode": { "type": "string" } }, "required": [ "locked", "viewMode" ] }, "example": { "gridLayout": [], "locked": false, "viewMode": "grid" } } } }, "500": { "description": "Failed to get dashboard preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_dashboard_preferences", "tags": [ "dashboard" ], "summary": "Update the current user's dashboard preferences (only provided fields are merged)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "gridLayout": { "type": "array", "items": { "type": "object", "properties": {} } }, "locked": { "type": "boolean" }, "viewMode": { "type": "string" } }, "required": [ "locked", "viewMode" ] }, "example": { "gridLayout": [ {} ], "locked": true, "viewMode": "string" } } } }, "500": { "description": "Failed to save dashboard preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "gridLayout": { "type": "array", "items": { "type": "object", "properties": {} } }, "locked": { "type": "boolean" }, "viewMode": { "type": "string" } } }, "example": { "locked": true, "viewMode": "list" } } } } } }, "/api/dashboard/stats": { "get": { "operationId": "get_api_dashboard_stats", "tags": [ "dashboard" ], "summary": "Get aggregated dashboard statistics per environment (containers, images, volumes, stacks, metrics)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Restrict the stats to a single environment (returns one object) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "online": { "type": "boolean" }, "containers": { "type": "object", "properties": {} }, "images": { "type": "object", "properties": {} }, "volumes": { "type": "object", "properties": {} }, "networks": { "type": "object", "properties": {} }, "stacks": { "type": "object", "properties": {} }, "metrics": { "type": "object", "properties": {} }, "events": { "type": "object", "properties": {} } }, "required": [ "id", "name" ] } }, "example": [ { "id": 0, "name": "string", "online": true, "containers": {}, "images": {}, "volumes": {}, "networks": {}, "stacks": {}, "metrics": {}, "events": {} } ] } } }, "403": { "description": "Permission denied (requires the environments:view permission)" }, "404": { "description": "Environment not found (when env is supplied)" }, "500": { "description": "Failed to get dashboard stats" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns an array of per-environment stats. When env is supplied and matches, a single object is returned instead of an array. An empty array is returned on a fresh install with no environments." } }, "/api/dashboard/stats/stream": { "get": { "operationId": "get_api_dashboard_stats_stream", "tags": [ "dashboard" ], "summary": "Stream per-environment dashboard stats progressively over Server-Sent Events", "parameters": [], "responses": { "200": { "description": "Server-Sent Events stream (text/event-stream), or a JSON payload when Accept application/json is requested" }, "403": { "description": "Permission denied (requires the environments:view permission)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Emits environments, partial, complete, error and done events as each environment's stats resolve. When the client sends Accept application/json, the stream is collected and returned as a single JSON payload instead." } }, "/api/debug/memory": { "get": { "operationId": "get_api_debug_memory", "tags": [ "debug" ], "summary": "Return Node.js/V8 memory diagnostics; only enabled when MEMORY_MONITOR=true. Also supports ?snapshot and ?snapshots to dump/list heap snapshots.", "parameters": [ { "name": "gc", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Force garbage collection before reporting when true (requires Node started with --expose-gc)" } ], "responses": { "200": { "description": "With ?snapshot returns {snapshot}, with ?snapshots returns {snapshots}; otherwise the full memory report", "content": { "application/json": { "schema": { "type": "object", "properties": { "timestamp": { "type": "string" }, "uptime": { "type": "string" }, "gcForced": { "type": "boolean" }, "gcAvailable": { "type": "boolean" }, "process": { "type": "string" }, "growth": { "type": "string" }, "v8Heap": { "type": "string" }, "system": { "type": "string" }, "rssTracker": { "type": "string" } }, "required": [ "timestamp", "uptime", "gcForced", "gcAvailable", "process", "growth", "v8Heap", "system" ] }, "example": { "timestamp": "2026-07-01T10:00:00.000Z", "uptime": { "ms": 3600000, "hours": 1, "human": "1h 0m" }, "gcForced": false, "gcAvailable": false, "process": { "rss": "180 MB" }, "growth": { "rssPerHour": "2 MB" }, "v8Heap": { "usedHeapSize": "90 MB" }, "system": { "cpus": 8, "platform": "linux" } } } } }, "403": { "description": "Memory monitor not enabled (set MEMORY_MONITOR=true)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/dependencies": { "get": { "operationId": "get_api_dependencies", "tags": [ "dependencies" ], "summary": "Return the combined list of npm dependencies and external tool images (grype, trivy), sorted by name, excluding Dockhand itself", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "version": { "type": "string" }, "license": { "type": "string" }, "repository": { "type": "string" } }, "required": [ "name", "version" ] } }, "example": [ { "name": "anchore/grype", "version": "v0.110.0", "license": "Apache-2.0", "repository": "https://github.com/anchore/grype" } ] } } } }, "security": [] } }, "/api/docs": { "get": { "operationId": "get_api_docs", "tags": [ "docs" ], "summary": "The full OpenAPI 3.0 specification for the Dockhand REST API (unauthenticated)", "parameters": [], "responses": { "200": { "description": "The generated OpenAPI document — see GET /api/docs/ui for an interactive viewer" }, "304": { "description": "Not modified — the client's If-None-Match matches the current spec ETag" }, "404": { "description": "Not found — API docs are disabled (set FEAT_API_DOCS=true to enable)" } }, "security": [] } }, "/api/environments": { "get": { "operationId": "get_api_environments", "tags": [ "environments" ], "summary": "List all Docker environments (hosts) known to Dockhand", "parameters": [], "responses": { "200": { "description": "Environments accessible to the caller (filtered by RBAC in Enterprise mode; all environments in Free edition). The tlsKey (private TLS client key) and hawserToken secrets are NEVER returned; hasTlsKey / hasHawserToken indicate whether one is stored.", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "connectionType": { "type": "string" }, "host": { "type": "string" }, "port": { "type": "integer" }, "protocol": { "type": "string" }, "icon": { "type": "string" }, "publicIp": { "type": "string" }, "timezone": { "type": "string" }, "hasTlsKey": { "type": "boolean" }, "hasHawserToken": { "type": "boolean" } }, "required": [ "id", "name", "connectionType" ] } }, "example": [ { "id": 1, "name": "hhdocker01", "connectionType": "socket", "host": null, "port": 2375, "protocol": "http", "icon": "server", "publicIp": "203.0.113.10", "timezone": "Europe/Berlin", "hasTlsKey": false, "hasHawserToken": false }, { "id": 2, "name": "hhdocker02", "connectionType": "hawser-edge", "host": null, "port": 2375, "protocol": "http", "icon": "server", "publicIp": null, "timezone": "Europe/Berlin", "hasTlsKey": false, "hasHawserToken": true } ] } } }, "403": { "description": "Permission denied (RBAC 'environments:view' missing)" }, "500": { "description": "Unexpected error while loading environments" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments", "tags": [ "environments" ], "summary": "Register a new Docker environment (host)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "connectionType": { "type": "string" } }, "required": [ "id", "name", "connectionType" ] }, "example": { "id": 3, "name": "hhdocker03", "connectionType": "socket" } } } }, "400": { "description": "Invalid name, invalid connectionType, or host missing for direct/hawser-standard" }, "403": { "description": "Permission denied (RBAC 'environments:create' missing)" }, "409": { "description": "An environment with this name already exists" }, "500": { "description": "Unexpected error while creating the environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "connectionType": { "type": "string" }, "host": { "type": "string" }, "port": { "type": "integer" }, "protocol": { "type": "string" }, "socketPath": { "type": "string" }, "icon": { "type": "string" }, "publicIp": { "type": "string" }, "labels": { "type": "string" } }, "required": [ "name" ] }, "example": { "name": "hhdocker03", "connectionType": "socket", "socketPath": "/var/run/docker.sock", "icon": "server" } } } } } }, "/api/environments/{id}": { "get": { "operationId": "get_api_environments_id", "tags": [ "environments" ], "summary": "Get a single environment by id, including its parsed labels and public IP", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "The tlsKey (private TLS client key) and hawserToken secrets are NEVER returned; hasTlsKey / hasHawserToken indicate whether one is stored.", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "connectionType": { "type": "string" }, "labels": { "type": "array", "items": { "type": "string" } }, "publicIp": { "type": "string" }, "hasTlsKey": { "type": "boolean" }, "hasHawserToken": { "type": "boolean" } }, "required": [ "id", "name", "connectionType" ] }, "example": { "id": 0, "name": "string", "connectionType": "string", "labels": [ "string" ], "publicIp": "string", "hasTlsKey": true, "hasHawserToken": true } } } }, "403": { "description": "Permission denied (RBAC 'environments:view' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while loading the environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_environments_id", "tags": [ "environments" ], "summary": "Update an environment; renaming also renames its on-disk stacks/git-repos directories", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "The tlsKey/hawserToken secrets are NEVER returned; hasTlsKey / hasHawserToken indicate whether one is stored.", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "connectionType": { "type": "string" }, "labels": { "type": "array", "items": { "type": "string" } }, "publicIp": { "type": "string" }, "hasTlsKey": { "type": "boolean" }, "hasHawserToken": { "type": "boolean" } }, "required": [ "id", "name", "connectionType" ] }, "example": { "id": 0, "name": "string", "connectionType": "string", "labels": [ "string" ], "publicIp": "string", "hasTlsKey": true, "hasHawserToken": true } } } }, "400": { "description": "Invalid new name (rename validation)" }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" }, "409": { "description": "Rename target directory already exists, or the on-disk rename failed (e.g. EXDEV across filesystems)" }, "500": { "description": "Unexpected error while updating the environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "host": { "type": "string" }, "port": { "type": "integer" }, "protocol": { "type": "string" }, "tlsCa": { "type": "string" }, "tlsCert": { "type": "string" }, "tlsKey": { "type": "string" }, "tlsSkipVerify": { "type": "boolean" }, "icon": { "type": "string" }, "socketPath": { "type": "string" }, "collectActivity": { "type": "boolean" }, "collectMetrics": { "type": "boolean" }, "highlightChanges": { "type": "boolean" }, "labels": { "type": "string" }, "connectionType": { "type": "string" }, "hawserToken": { "type": "string" }, "publicIp": { "type": "string" } } }, "example": { "name": "hhdocker03", "collectMetrics": true } } } } }, "delete": { "operationId": "delete_api_environments_id", "tags": [ "environments" ], "summary": "Delete an environment and all its associated git stacks, schedules, icons, and on-disk directories", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid environment id, or the environment could not be deleted" }, "403": { "description": "Permission denied (RBAC 'environments:delete' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Environment name is empty/whitespace (refuses to delete to avoid an unsafe directory cleanup), or an unexpected error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/environments/{id}/disk-warning": { "get": { "operationId": "get_api_environments_id_disk-warning", "tags": [ "environments" ], "summary": "Get the disk-space warning thresholds for an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "mode": { "type": "string" }, "threshold": { "type": "integer" }, "thresholdGb": { "type": "integer" } }, "required": [ "enabled", "mode", "threshold", "thresholdGb" ] }, "example": { "enabled": true, "mode": "percentage", "threshold": 80, "thresholdGb": 50 } } } }, "403": { "description": "Permission denied (RBAC 'environments:view' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while loading the settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments_id_disk-warning", "tags": [ "environments" ], "summary": "Save the disk-space warning thresholds for an environment (each field optional/independent)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while saving the settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "mode": { "type": "string" }, "threshold": { "type": "integer" }, "thresholdGb": { "type": "integer" } } }, "example": { "enabled": true, "mode": "percentage", "threshold": 85 } } } } } }, "/api/environments/{id}/icon": { "get": { "operationId": "get_api_environments_id_icon", "tags": [ "environments" ], "summary": "Get the custom icon image for an environment (raw image/webp bytes, not JSON)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Binary image/webp response body, Cache-Control public max-age=3600" }, "404": { "description": "No custom icon set for this environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments_id_icon", "tags": [ "environments" ], "summary": "Upload a custom icon for an environment (base64-encoded image, ~200KB limit)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "icon": { "type": "string" } }, "required": [ "success", "icon" ] }, "example": { "success": true, "icon": "custom:env-3.webp" } } } }, "400": { "description": "Missing image data, or image exceeds the ~300000-char base64 size limit" }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "image": { "type": "string" } }, "required": [ "image" ] }, "example": { "image": "string" } } } } }, "delete": { "operationId": "delete_api_environments_id_icon", "tags": [ "environments" ], "summary": "Remove an environment's custom icon and reset it to the default \"globe\" icon", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "icon": { "type": "string" } }, "required": [ "success", "icon" ] }, "example": { "success": true, "icon": "globe" } } } }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/environments/{id}/image-prune": { "get": { "operationId": "get_api_environments_id_image-prune", "tags": [ "environments" ], "summary": "Get the automatic image-prune schedule settings for an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "settings": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "cronExpression": { "type": "string" }, "pruneMode": { "type": "string" } }, "required": [ "enabled", "cronExpression", "pruneMode" ] } }, "required": [ "settings" ] }, "example": { "settings": { "enabled": false, "cronExpression": "0 3 * * 0", "pruneMode": "dangling" } } } } }, "403": { "description": "Permission denied (RBAC 'environments:view' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while loading the settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments_id_image-prune", "tags": [ "environments" ], "summary": "Save the automatic image-prune schedule for an environment (registers/unregisters the croner job)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "settings": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "cronExpression": { "type": "string" }, "pruneMode": { "type": "string" } }, "required": [ "enabled", "cronExpression", "pruneMode" ] } }, "required": [ "success", "settings" ] }, "example": { "success": true, "settings": { "enabled": true, "cronExpression": "string", "pruneMode": "string" } } } } }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while saving the settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "cronExpression": { "type": "string" }, "pruneMode": { "type": "string" } } }, "example": { "enabled": true, "cronExpression": "0 3 * * 0", "pruneMode": "dangling" } } } } }, "put": { "operationId": "put_api_environments_id_image-prune", "tags": [ "environments" ], "summary": "Immediately run an image prune for an environment (outside its schedule)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The prune operation itself failed (Docker error)" }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while triggering the prune" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/environments/{id}/notifications": { "get": { "operationId": "get_api_environments_id_notifications", "tags": [ "environments" ], "summary": "List notification channel configurations attached to an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "notificationId": { "type": "integer" }, "enabled": { "type": "boolean" }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "id", "notificationId", "enabled" ] } }, "example": [ { "id": 0, "notificationId": 0, "enabled": true, "eventTypes": [ "string" ] } ] } } }, "400": { "description": "Invalid environment id" }, "403": { "description": "Permission denied (RBAC 'notifications:view' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while loading notifications" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments_id_notifications", "tags": [ "environments" ], "summary": "Attach a notification channel to an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "notificationId": { "type": "integer" }, "enabled": { "type": "boolean" } }, "required": [ "id", "notificationId", "enabled" ] }, "example": { "id": 0, "notificationId": 0, "enabled": true } } } }, "400": { "description": "Invalid environment id, or notificationId missing" }, "403": { "description": "Permission denied (RBAC 'notifications:edit' missing)" }, "404": { "description": "Environment not found" }, "409": { "description": "This notification channel is already configured for this environment" }, "500": { "description": "Unexpected error while creating the notification link" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "notificationId from GET /api/notifications.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "notificationId": { "type": "integer" }, "enabled": { "type": "boolean" }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "notificationId" ] }, "example": { "notificationId": 1, "enabled": true, "eventTypes": [ "stack_deploy_failed" ] } } } } } }, "/api/environments/{id}/notifications/{notificationId}": { "get": { "operationId": "get_api_environments_id_notifications_notificationId", "tags": [ "environments" ], "summary": "Get one notification-channel link for an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" }, { "name": "notificationId", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Notification channel id (from GET /api/notifications)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "notificationId": { "type": "integer" }, "enabled": { "type": "boolean" }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "id", "notificationId", "enabled" ] }, "example": { "id": 0, "notificationId": 0, "enabled": true, "eventTypes": [ "string" ] } } } }, "400": { "description": "Invalid id or notificationId" }, "403": { "description": "Permission denied (RBAC 'notifications:view' missing)" }, "404": { "description": "Environment not found, or no such notification link for this environment" }, "500": { "description": "Unexpected error while loading the notification link" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_environments_id_notifications_notificationId", "tags": [ "environments" ], "summary": "Update a notification-channel link (enabled state and/or event type filter)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" }, { "name": "notificationId", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Notification channel id (from GET /api/notifications)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "notificationId": { "type": "integer" }, "enabled": { "type": "boolean" }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "id", "notificationId", "enabled" ] }, "example": { "id": 0, "notificationId": 0, "enabled": true, "eventTypes": [ "string" ] } } } }, "400": { "description": "Invalid id or notificationId" }, "403": { "description": "Permission denied (RBAC 'notifications:edit' missing)" }, "404": { "description": "Environment not found, or no such notification link for this environment" }, "500": { "description": "Unexpected error while updating the notification link" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "eventTypes": { "type": "array", "items": { "type": "string" } } } }, "example": { "enabled": false } } } } }, "delete": { "operationId": "delete_api_environments_id_notifications_notificationId", "tags": [ "environments" ], "summary": "Remove a notification-channel link from an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" }, { "name": "notificationId", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Notification channel id (from GET /api/notifications)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid id or notificationId" }, "403": { "description": "Permission denied (RBAC 'notifications:delete' missing)" }, "404": { "description": "No such notification link for this environment" }, "500": { "description": "Unexpected error while deleting the notification link" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/environments/{id}/remote-stacks-dir": { "get": { "operationId": "get_api_environments_id_remote-stacks-dir", "tags": [ "environments" ], "summary": "Get the per-environment remote stacks directory used to stage stack files onto a direct (agentless) daemon", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment ID (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "remoteStacksDir": { "type": "string" } } }, "example": { "remoteStacksDir": "/mnt/dockhand/stacks" } } } }, "403": { "description": "Permission denied (requires environments:view)" }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments_id_remote-stacks-dir", "tags": [ "environments" ], "summary": "Set (or clear) the per-environment remote stacks directory for a direct (agentless) daemon", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment ID (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "remoteStacksDir": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true, "remoteStacksDir": "/mnt/dockhand/stacks" } } } }, "400": { "description": "Invalid input — remoteStacksDir must be a string or null, or must be an absolute path with no \"..\"" }, "403": { "description": "Permission denied (requires environments:edit)" }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "remoteStacksDir": { "type": "string" } } }, "example": { "remoteStacksDir": "/mnt/dockhand/stacks" } } } } } }, "/api/environments/{id}/test": { "post": { "operationId": "post_api_environments_id_test", "tags": [ "environments" ], "summary": "Test connectivity to a saved environment's Docker/Hawser endpoint", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "success:false with a human-readable error message is also returned as HTTP 200 (connection/agent-not-connected states are not transport errors)", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "info": { "type": "object", "properties": { "serverVersion": { "type": "string" }, "containers": { "type": "integer" }, "images": { "type": "integer" }, "name": { "type": "string" }, "engine": { "type": "string" } } }, "isEdgeMode": { "type": "boolean" }, "hawser": { "type": "object", "properties": {} } }, "required": [ "success" ] }, "example": { "success": true, "info": { "serverVersion": "string", "containers": 0, "images": 0, "name": "string", "engine": "string" }, "isEdgeMode": true, "hawser": {} } } } }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/environments/{id}/timezone": { "get": { "operationId": "get_api_environments_id_timezone", "tags": [ "environments" ], "summary": "Get the IANA timezone used for scheduling on an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "timezone": { "type": "string" } }, "required": [ "timezone" ] }, "example": { "timezone": "Europe/Berlin" } } } }, "403": { "description": "Permission denied (RBAC 'environments:view' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while loading the timezone" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments_id_timezone", "tags": [ "environments" ], "summary": "Set the IANA timezone for an environment and refresh its schedules to use it", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "timezone": { "type": "string" } }, "required": [ "success", "timezone" ] }, "example": { "success": true, "timezone": "string" } } } }, "400": { "description": "Not a recognized IANA timezone (Intl.supportedValuesOf('timeZone'))" }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while saving the timezone" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "timezone": { "type": "string" } }, "required": [ "timezone" ] }, "example": { "timezone": "Europe/Berlin" } } } } } }, "/api/environments/{id}/update-check": { "get": { "operationId": "get_api_environments_id_update-check", "tags": [ "environments" ], "summary": "Get the automatic container-image update-check schedule for an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "settings": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "cron": { "type": "string" }, "autoUpdate": { "type": "boolean" }, "vulnerabilityCriteria": { "type": "string" } }, "required": [ "enabled", "cron", "autoUpdate", "vulnerabilityCriteria" ] } }, "required": [ "settings" ] }, "example": { "settings": { "enabled": false, "cron": "0 4 * * *", "autoUpdate": false, "vulnerabilityCriteria": "never" } } } } }, "403": { "description": "Permission denied (RBAC 'environments:view' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while loading the settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_environments_id_update-check", "tags": [ "environments" ], "summary": "Save the automatic image update-check schedule for an environment (registers/unregisters the croner job)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "settings": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "cron": { "type": "string" }, "autoUpdate": { "type": "boolean" }, "vulnerabilityCriteria": { "type": "string" } }, "required": [ "enabled", "cron", "autoUpdate", "vulnerabilityCriteria" ] } }, "required": [ "success", "settings" ] }, "example": { "success": true, "settings": { "enabled": true, "cron": "string", "autoUpdate": true, "vulnerabilityCriteria": "string" } } } } }, "403": { "description": "Permission denied (RBAC 'environments:edit' missing)" }, "404": { "description": "Environment not found" }, "500": { "description": "Unexpected error while saving the settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "cron": { "type": "string" }, "autoUpdate": { "type": "boolean" }, "vulnerabilityCriteria": { "type": "string" } } }, "example": { "enabled": true, "cron": "0 4 * * *", "autoUpdate": false, "vulnerabilityCriteria": "never" } } } } } }, "/api/environments/detect-socket": { "get": { "operationId": "get_api_environments_detect-socket", "tags": [ "environments" ], "summary": "Detect common Docker/Podman socket paths that exist on the Dockhand host", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "sockets": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "name": { "type": "string" }, "exists": { "type": "boolean" } }, "required": [ "path", "name", "exists" ] } }, "homedir": { "type": "string" } }, "required": [ "sockets", "homedir" ] }, "example": { "sockets": [ { "path": "/var/run/docker.sock", "name": "Docker (default)", "exists": true } ], "homedir": "/home/dockhand" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/environments/test": { "post": { "operationId": "post_api_environments_test", "tags": [ "environments" ], "summary": "Test a Docker/Hawser connection configuration WITHOUT saving it as an environment", "parameters": [], "responses": { "200": { "description": "success:false with a human-readable error message is also returned as HTTP 200 (connection failures are not transport errors)", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "info": { "type": "object", "properties": { "serverVersion": { "type": "string" }, "containers": { "type": "integer" }, "images": { "type": "integer" }, "name": { "type": "string" } } }, "hawser": { "type": "object", "properties": {} } }, "required": [ "success" ] }, "example": { "success": true, "info": { "serverVersion": "string", "containers": 0, "images": 0, "name": "string" }, "hawser": {} } } } }, "400": { "description": "Host is required for direct/hawser-standard connection types" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "connectionType": { "type": "string" }, "socketPath": { "type": "string" }, "host": { "type": "string" }, "port": { "type": "integer" }, "protocol": { "type": "string" }, "tlsCa": { "type": "string" }, "tlsCert": { "type": "string" }, "tlsKey": { "type": "string" }, "tlsSkipVerify": { "type": "boolean" }, "hawserToken": { "type": "string" } }, "required": [ "connectionType" ] }, "example": { "connectionType": "socket", "socketPath": "/var/run/docker.sock" } } } } } }, "/api/events": { "get": { "operationId": "get_api_events", "tags": [ "events" ], "summary": "Stream live Docker events (container/image/volume/network) for an environment via SSE, with periodic heartbeats", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id — without it, an \"info\" SSE message is sent and the stream ends (from GET /api/environments)" } ], "responses": { "200": { "description": "text/event-stream SSE stream (\"connected\", \"heartbeat\" every 5s, \"docker\" events with {type,action,actor,time,timeNano}, or an \"error\"/\"info\" event for edge environments, missing/unknown environment, or a lost Docker connection)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/branches": { "post": { "operationId": "post_api_git_branches", "tags": [ "git" ], "summary": "List remote branches for a git repository via `git ls-remote`", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "branches": { "type": "array", "items": { "type": "string" } } }, "required": [ "branches" ] }, "example": { "branches": [ { "name": "main", "sha": "a1b2c3d" }, { "name": "develop", "sha": "e4f5a6b" } ] } } } }, "400": { "description": "The URL points at a loopback/link-local/metadata/reserved target, or neither repositoryId nor url was supplied" }, "403": { "description": "Permission denied (requires git:edit)" }, "404": { "description": "The referenced repository does not exist" }, "500": { "description": "Failed to fetch branches (ls-remote error or timeout)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Accepts either repositoryId (an existing repository, using its stored URL and credential) or url + credentialId (a new repository). Each branch carries its short commit SHA. SECURITY: the repository target is checked against the shared SSRF policy — loopback, link-local/cloud-metadata and other reserved dangerous targets are rejected, while ordinary private-LAN addresses are intentionally allowed so self-hosted Git servers remain supported. The ls-remote is bounded by a hard timeout.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "repositoryId": { "type": "integer" }, "url": { "type": "string" }, "credentialId": { "type": "integer" } } }, "example": { "url": "https://github.com/example/repo.git", "credentialId": 2 } } } } } }, "/api/git/credentials": { "get": { "operationId": "get_api_git_credentials", "tags": [ "git" ], "summary": "List all stored git credentials with secrets stripped (only hasPassword/hasSshKey flags returned)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "authType": { "type": "string" }, "username": { "type": "string" }, "hasPassword": { "type": "boolean" }, "hasSshKey": { "type": "boolean" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "authType", "hasPassword", "hasSshKey" ] } }, "example": [ { "id": 1, "name": "github-deploy", "authType": "ssh", "username": "git", "hasPassword": false, "hasSshKey": true, "createdAt": "2026-06-01T10:00:00Z", "updatedAt": "2026-06-01T10:00:00Z" } ] } } }, "403": { "description": "Caller lacks the git:view permission" }, "500": { "description": "Failed to read git credentials from the database" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_git_credentials", "tags": [ "git" ], "summary": "Create a new git credential (none/password/ssh auth) and return it with secrets stripped", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "authType": { "type": "string" }, "username": { "type": "string" }, "hasPassword": { "type": "boolean" }, "hasSshKey": { "type": "boolean" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "authType", "hasPassword", "hasSshKey" ] }, "example": { "id": 0, "name": "string", "authType": "string", "username": "string", "hasPassword": true, "hasSshKey": true, "createdAt": "string", "updatedAt": "string" } } } }, "400": { "description": "Missing name, invalid authType, a missing secret for the chosen authType, or a duplicate credential name" }, "403": { "description": "Caller lacks the git:create permission" }, "500": { "description": "Failed to create the git credential" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "authType": { "type": "string" }, "username": { "type": "string" }, "password": { "type": "string" }, "sshPrivateKey": { "type": "string" }, "sshPassphrase": { "type": "string" } }, "required": [ "name" ] }, "example": { "name": "github-deploy", "authType": "ssh", "username": "git", "sshPrivateKey": "***", "sshPassphrase": "***" } } } } } }, "/api/git/credentials/{id}": { "get": { "operationId": "get_api_git_credentials_id", "tags": [ "git" ], "summary": "Get a single git credential by ID with secrets stripped (only hasPassword/hasSshKey flags returned)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git credential ID (from GET /api/git/credentials)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "authType": { "type": "string" }, "username": { "type": "string" }, "hasPassword": { "type": "boolean" }, "hasSshKey": { "type": "boolean" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "authType", "hasPassword", "hasSshKey" ] }, "example": { "id": 0, "name": "string", "authType": "string", "username": "string", "hasPassword": true, "hasSshKey": true, "createdAt": "string", "updatedAt": "string" } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:view permission" }, "404": { "description": "No credential exists with that ID" }, "500": { "description": "Failed to read the git credential" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_git_credentials_id", "tags": [ "git" ], "summary": "Update a git credential and return it with secrets stripped", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git credential ID (from GET /api/git/credentials)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "authType": { "type": "string" }, "username": { "type": "string" }, "hasPassword": { "type": "boolean" }, "hasSshKey": { "type": "boolean" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "name", "authType", "hasPassword", "hasSshKey" ] }, "example": { "id": 0, "name": "string", "authType": "string", "username": "string", "hasPassword": true, "hasSshKey": true, "createdAt": "string", "updatedAt": "string" } } } }, "400": { "description": "Invalid id, invalid authType, or a duplicate credential name" }, "403": { "description": "Caller lacks the git:edit permission" }, "404": { "description": "No credential exists with that ID" }, "500": { "description": "The update failed or the credential could not be persisted" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "authType": { "type": "string" }, "username": { "type": "string" }, "password": { "type": "string" }, "sshPrivateKey": { "type": "string" }, "sshPassphrase": { "type": "string" } } }, "example": { "name": "github-deploy", "authType": "password", "username": "git", "password": "***" } } } } }, "delete": { "operationId": "delete_api_git_credentials_id", "tags": [ "git" ], "summary": "Delete a git credential by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git credential ID (from GET /api/git/credentials)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:delete permission" }, "404": { "description": "No credential exists with that ID" }, "500": { "description": "The deletion failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/preview-env": { "post": { "operationId": "post_api_git_preview-env", "tags": [ "git" ], "summary": "Clone a repo to a temp dir and preview its merged env-file variables for the git-stack env editor", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "vars": { "type": "string" }, "sources": { "type": "string" } }, "required": [ "vars", "sources" ] }, "example": { "vars": "string", "sources": "string" } } } }, "400": { "description": "composePath missing, neither repositoryId nor url supplied, the URL points at a loopback/link-local/metadata/reserved target, the URL is an unsupported transport (ext::/file::), the compose/env path escapes the repository, or the repo/env-file preview reported an error" }, "403": { "description": "Permission denied (requires the git:edit permission — same model as /api/git/branches; an unauthenticated or read-only user is denied here rather than via a separate 401)" }, "404": { "description": "The referenced repository does not exist" }, "500": { "description": "Failed to preview the env files (clone or read error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "repositoryId from GET /api/git/repositories. credentialId from GET /api/git/credentials. SECURITY: requires the git:edit permission when authentication is enabled (403 otherwise). The repository target is checked against the shared SSRF policy — loopback, link-local/cloud-metadata and other reserved dangerous targets are rejected, while ordinary private-LAN addresses are intentionally allowed so self-hosted Git servers remain supported. The ext::/file:: transports and local paths are rejected; and composePath/envFilePath are constrained to stay inside the cloned repository (path traversal).", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "repositoryId": { "type": "integer" }, "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" }, "composePath": { "type": "string" }, "envFilePath": { "type": "string" } }, "required": [ "composePath" ] }, "example": { "repositoryId": 3, "composePath": "docker-compose.yml", "envFilePath": ".env.prod" } } } } } }, "/api/git/repositories": { "get": { "operationId": "get_api_git_repositories", "tags": [ "git" ], "summary": "List all git repositories (repositories are global, not scoped to an environment)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" } }, "required": [ "id", "name", "url", "branch" ] } }, "example": [ { "id": 1, "name": "homelab", "url": "https://github.com/example/homelab.git", "branch": "main", "credentialId": 2 } ] } } }, "403": { "description": "Caller lacks the git:view permission" }, "500": { "description": "Failed to read git repositories" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_git_repositories", "tags": [ "git" ], "summary": "Create a git repository (branch defaults to main); deployment config lives on git stacks, not here", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" } }, "required": [ "id", "name", "url", "branch" ] }, "example": { "id": 0, "name": "string", "url": "string", "branch": "string", "credentialId": 0 } } } }, "400": { "description": "Missing name/url, an invalid credentialId, or a duplicate repository name" }, "403": { "description": "Caller lacks the git:create permission" }, "500": { "description": "Failed to create the git repository" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "credentialId from GET /api/git/credentials.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" } }, "required": [ "name", "url" ] }, "example": { "name": "homelab", "url": "https://github.com/example/homelab.git", "branch": "main", "credentialId": 2 } } } } } }, "/api/git/repositories/{id}": { "get": { "operationId": "get_api_git_repositories_id", "tags": [ "git" ], "summary": "Get a single git repository by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" } }, "required": [ "id", "name", "url", "branch" ] }, "example": { "id": 0, "name": "string", "url": "string", "branch": "string", "credentialId": 0 } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:view permission" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "Failed to read the git repository" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_git_repositories_id", "tags": [ "git" ], "summary": "Update a git repository's basic fields (name/url/branch/credential)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" } }, "required": [ "id", "name", "url", "branch" ] }, "example": { "id": 0, "name": "string", "url": "string", "branch": "string", "credentialId": 0 } } } }, "400": { "description": "Invalid id, an invalid credentialId, or a duplicate repository name" }, "403": { "description": "Caller lacks the git:edit permission" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The update failed or the repository could not be persisted" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "credentialId from GET /api/git/credentials.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" } } }, "example": { "name": "homelab", "url": "https://github.com/example/homelab.git", "branch": "production", "credentialId": 2 } } } } }, "delete": { "operationId": "delete_api_git_repositories_id", "tags": [ "git" ], "summary": "Delete a git repository, first removing the clone directories of every git stack it backs", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:delete permission" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The deletion failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/repositories/{id}/deploy": { "post": { "operationId": "post_api_git_repositories_id_deploy", "tags": [ "git" ], "summary": "Deploy the compose stack(s) defined in a git repository (clones/pulls, then runs docker compose)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:edit permission" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The deployment failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/repositories/{id}/sync": { "get": { "operationId": "get_api_git_repositories_id_sync", "tags": [ "git" ], "summary": "Check whether the tracked branch has new commits upstream without pulling them", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "hasUpdates": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "hasUpdates" ] }, "example": { "hasUpdates": false } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:view permission" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The update check failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_git_repositories_id_sync", "tags": [ "git" ], "summary": "Sync (git pull) the local clone of a repository to the latest commit on its tracked branch", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:edit permission" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The sync failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/repositories/{id}/test": { "post": { "operationId": "post_api_git_repositories_id_test", "tags": [ "git" ], "summary": "Test connectivity/authentication to a saved repository using its stored credential", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the git:edit permission" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The connectivity test failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/repositories/test": { "post": { "operationId": "post_api_git_repositories_test", "tags": [ "git" ], "summary": "Test an unsaved repository configuration (url/branch/credentialId) before creating it", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The url field is missing, the URL points at a loopback/link-local/metadata/reserved target, the URL is an unsupported transport." }, "403": { "description": "Caller lacks the settings:manage permission" }, "404": { "description": "The referenced credential does not exist" }, "500": { "description": "The connectivity test failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "credentialId from GET /api/git/credentials. SECURITY: the repository target is checked against the shared SSRF policy — loopback, link-local/cloud-metadata and other reserved dangerous targets are rejected, while ordinary private-LAN addresses are intentionally allowed so self-hosted Git servers remain supported. The ext::/file:: transports and local paths are rejected.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string" }, "branch": { "type": "string" }, "credentialId": { "type": "integer" } }, "required": [ "url" ] }, "example": { "url": "https://github.com/example/homelab.git", "branch": "main", "credentialId": 2 } } } } } }, "/api/git/stacks": { "get": { "operationId": "get_api_git_stacks", "tags": [ "git" ], "summary": "List git-deployed stacks (optionally scoped to one environment)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter to a single environment id" } ], "responses": { "200": { "description": "Successful response" }, "403": { "description": "Permission denied (needs stacks:view)" }, "500": { "description": "Failed to list git stacks" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_git_stacks", "tags": [ "git" ], "summary": "Create a git-deployed stack (from an existing repo or new repo url/branch)", "parameters": [], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Invalid stack name, or secretProviderId is not a number/null" }, "403": { "description": "Permission denied (needs stacks:create; binding a secret provider also needs secrets:view)" }, "409": { "description": "A git stack with this name already exists in the environment" }, "500": { "description": "Failed to create the git stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "stackName": { "type": "string" }, "environmentId": { "type": "integer" }, "repositoryId": { "type": "integer" }, "secretProviderId": { "type": "integer" }, "webhookEnabled": { "type": "boolean" }, "webhookSecret": { "type": "string" } }, "required": [ "stackName" ] }, "example": { "stackName": "string", "environmentId": 0, "repositoryId": 0, "secretProviderId": 0, "webhookEnabled": true, "webhookSecret": "string" } } } } } }, "/api/git/stacks/{id}": { "get": { "operationId": "get_api_git_stacks_id", "tags": [ "git" ], "summary": "Get one git stack", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The git stack id" } ], "responses": { "200": { "description": "Successful response" }, "403": { "description": "Permission denied (needs stacks:view)" }, "404": { "description": "Git stack not found" }, "500": { "description": "Failed to load the git stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_git_stacks_id", "tags": [ "git" ], "summary": "Update a git stack (rename, schedule, webhook, secret-provider binding)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The git stack id" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Invalid stack name, or secretProviderId is not a number/null" }, "403": { "description": "Permission denied (needs stacks:edit; binding a secret provider also needs secrets:view)" }, "404": { "description": "Git stack not found" }, "500": { "description": "Failed to update the git stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "stackName": { "type": "string" }, "secretProviderId": { "type": "integer" }, "webhookEnabled": { "type": "boolean" }, "webhookSecret": { "type": "string" } } }, "example": { "stackName": "string", "secretProviderId": 0, "webhookEnabled": true, "webhookSecret": "string" } } } } }, "delete": { "operationId": "delete_api_git_stacks_id", "tags": [ "git" ], "summary": "Delete a git stack (removes its deploy files and stack source)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The git stack id" } ], "responses": { "200": { "description": "Successful response" }, "403": { "description": "Permission denied (needs stacks:delete)" }, "404": { "description": "Git stack not found" }, "500": { "description": "Failed to delete the git stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/stacks/{id}/deploy": { "post": { "operationId": "post_api_git_stacks_id_deploy", "tags": [ "git" ], "summary": "Deploy a git stack, streaming the deploy log as SSE result events via the job-response channel", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" } ], "responses": { "200": { "description": "An SSE stream whose final `result` event carries {success, error}", "content": { "application/json": { "schema": { "type": "object", "properties": {} }, "example": {} } } }, "403": { "description": "Caller lacks the stacks:start permission for the stack's environment" }, "404": { "description": "No git stack exists with that ID" }, "500": { "description": "Failed to start the deployment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/stacks/{id}/deploy-stream": { "post": { "operationId": "post_api_git_stacks_id_deploy-stream", "tags": [ "git" ], "summary": "Deploy a git stack with live progress; streams SSE, or returns a jobId to poll (Accept negotiated)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" } ], "responses": { "200": { "description": "Deployment started — either a jobId to poll or a streamed SSE deploy log", "content": { "application/json": { "schema": { "type": "object", "properties": { "jobId": { "type": "string" } } }, "example": { "jobId": "a1b2c3d4" } } } }, "403": { "description": "Caller lacks the stacks:start permission for the stack's environment" }, "404": { "description": "No git stack exists with that ID" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Clients sending `Accept: application/json` get a synchronous, buffered SSE-as-JSON result; otherwise a jobId is returned immediately and progress is delivered out-of-band." } }, "/api/git/stacks/{id}/env-files": { "get": { "operationId": "get_api_git_stacks_id_env-files", "tags": [ "git" ], "summary": "List the .env files present in a git stack's cloned repository", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "files": { "type": "array", "items": { "type": "string" } } }, "required": [ "files" ] }, "example": { "files": [ ".env", ".env.prod" ] } } } }, "400": { "description": "The repository could not be read (e.g. not yet cloned)" }, "403": { "description": "Caller lacks the stacks:view permission for the stack's environment" }, "404": { "description": "No git stack exists with that ID" }, "500": { "description": "Failed to list the env files" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Lists the `.env*` files in the git stack's synced repository checkout (read-only). Use this to discover which file to configure as the stack's `envFilePath`. This endpoint does not write — to choose the env file, PUT /api/git/stacks/{id} with `envFilePath`; to set env values, use the stack env endpoints." }, "post": { "operationId": "post_api_git_stacks_id_env-files", "tags": [ "git" ], "summary": "Read and parse one .env file from a git stack's repository into a key/value map", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "vars": { "type": "string" } }, "required": [ "vars" ] }, "example": { "vars": { "TZ": "Europe/Berlin" } } } } }, "400": { "description": "The path field is missing or the env file could not be read" }, "403": { "description": "Caller lacks the stacks:view permission for the stack's environment" }, "404": { "description": "No git stack exists with that ID" }, "500": { "description": "Failed to read the env file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Reads and parses a single `.env` file (by `path`) from the git stack's synced repository checkout into a key/value map (read-only). There is no write counterpart — git-stack env is set via `envFilePath` (PUT /api/git/stacks/{id}) or the stack env endpoints.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" } }, "required": [ "path" ] }, "example": { "path": ".env.prod" } } } } } }, "/api/git/stacks/{id}/sync": { "post": { "operationId": "post_api_git_stacks_id_sync", "tags": [ "git" ], "summary": "Sync (git pull) a git stack's repository clone to the latest tracked commit", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Caller lacks the stacks:edit permission for the stack's environment" }, "404": { "description": "No git stack exists with that ID" }, "500": { "description": "The sync failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/stacks/{id}/test": { "post": { "operationId": "post_api_git_stacks_id_test", "tags": [ "git" ], "summary": "Test a git stack's repository access and compose configuration without deploying", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Caller lacks the stacks:view permission for the stack's environment" }, "404": { "description": "No git stack exists with that ID" }, "500": { "description": "The test failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/git/stacks/{id}/webhook": { "get": { "operationId": "get_api_git_stacks_id_webhook", "tags": [ "git" ], "summary": "GET webhook trigger for a git stack, with the secret passed as the `secret` query parameter", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" }, { "name": "secret", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Webhook secret; required only if the stack has a webhook secret configured" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "skipped": { "type": "boolean" }, "error": { "type": "string" } } }, "example": { "success": true, "skipped": false } } } }, "400": { "description": "The id path segment is not a valid integer" }, "401": { "description": "The provided secret did not match the stack's webhook secret" }, "403": { "description": "Webhooks are not enabled for this stack" }, "404": { "description": "No git stack exists with that ID" }, "500": { "description": "The deployment triggered by the webhook failed" } }, "security": [] }, "post": { "operationId": "post_api_git_stacks_id_webhook", "tags": [ "git" ], "summary": "Webhook trigger (GitHub/GitLab) that deploys a git stack when its signature/token verifies", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git stack ID (from GET /api/git/stacks)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "skipped": { "type": "boolean" }, "error": { "type": "string" } } }, "example": { "success": true, "skipped": false } } } }, "400": { "description": "The id path segment is not a valid integer" }, "401": { "description": "The webhook signature or token did not verify" }, "403": { "description": "Webhooks are not enabled for this stack" }, "404": { "description": "No git stack exists with that ID" }, "500": { "description": "The deployment triggered by the webhook failed" } }, "security": [], "description": "Public endpoint authenticated by the stack's webhook secret via `X-Hub-Signature-256` (GitHub) or `X-Gitlab-Token` (GitLab); the raw request body is used for HMAC verification." } }, "/api/git/webhook/{id}": { "get": { "operationId": "get_api_git_webhook_id", "tags": [ "git" ], "summary": "GET webhook trigger for a git repository, with the secret passed as the `secret` query parameter", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" }, { "name": "secret", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Webhook secret; required only if the repository has a webhook secret configured" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } } }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "401": { "description": "The provided secret did not match the repository's webhook secret" }, "403": { "description": "Webhooks are not enabled for this repository" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The deployment triggered by the webhook failed" } }, "security": [], "description": "A secret is required by default; set ALLOW_WEBHOOKS_WITHOUT_SECRET=true to accept a secret-less trigger on an isolated network." }, "post": { "operationId": "post_api_git_webhook_id", "tags": [ "git" ], "summary": "Webhook trigger (GitHub/GitLab) that deploys from a git repository when its signature/token verifies", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Git repository ID (from GET /api/git/repositories)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } } }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "401": { "description": "The webhook signature or token did not verify" }, "403": { "description": "Webhooks are not enabled for this repository" }, "404": { "description": "No repository exists with that ID" }, "500": { "description": "The deployment triggered by the webhook failed" } }, "security": [], "description": "Public endpoint authenticated by the repository's webhook secret via `X-Hub-Signature-256` (GitHub) or `X-Gitlab-Token` (GitLab); the raw request body is used for HMAC verification. A secret is required by default; set ALLOW_WEBHOOKS_WITHOUT_SECRET=true to accept a secret-less trigger on an isolated network." } }, "/api/hawser/connect": { "get": { "operationId": "get_api_hawser_connect", "tags": [ "hawser" ], "summary": "Report the Hawser Edge WebSocket endpoint status and list currently connected agents", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "message": { "type": "string" }, "protocol": { "type": "string" }, "activeConnections": { "type": "integer" }, "connections": { "type": "array", "items": { "type": "object", "properties": { "environmentId": { "type": "integer" }, "agentId": { "type": "string" }, "agentName": { "type": "string" }, "agentVersion": { "type": "string" }, "dockerVersion": { "type": "string" }, "hostname": { "type": "string" }, "capabilities": { "type": "array", "items": { "type": "string" } }, "connectedAt": { "type": "string" }, "lastHeartbeat": { "type": "string" } }, "required": [ "environmentId" ] } } }, "required": [ "status", "message", "protocol", "activeConnections", "connections" ] }, "example": { "status": "ready", "message": "Hawser Edge WebSocket endpoint. Connect via WebSocket.", "protocol": "wss:///api/hawser/connect", "activeConnections": 0, "connections": [] } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_hawser_connect", "tags": [ "hawser" ], "summary": "Fallback for non-WebSocket clients; always advises upgrading to a WebSocket connection", "parameters": [], "responses": { "200": { "description": "Successful response" }, "426": { "description": "This endpoint requires a WebSocket upgrade (ws:// or wss://)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/hawser/tokens": { "get": { "operationId": "get_api_hawser_tokens", "tags": [ "hawser" ], "summary": "List all Hawser agent tokens (only the prefix is returned, never the full token)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "tokenPrefix": { "type": "string" }, "name": { "type": "string" }, "environmentId": { "type": "integer" }, "isActive": { "type": "boolean" }, "lastUsed": { "type": "string" }, "createdAt": { "type": "string" }, "expiresAt": { "type": "string" } }, "required": [ "id", "tokenPrefix", "name", "environmentId", "isActive", "createdAt" ] } }, "example": [ { "id": 1, "tokenPrefix": "hw_abc123", "name": "edge-01", "environmentId": 1, "isActive": true, "lastUsed": null, "createdAt": "2026-06-01T10:00:00Z", "expiresAt": null } ] } } }, "401": { "description": "Not authenticated" }, "403": { "description": "Admin access required" }, "500": { "description": "Failed to read the tokens" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_hawser_tokens", "tags": [ "hawser" ], "summary": "Generate a new Hawser agent token for an environment (the plaintext token is returned only once)", "parameters": [], "responses": { "200": { "description": "Token generated — the plaintext token is shown only once and cannot be retrieved again", "content": { "application/json": { "schema": { "type": "object", "properties": { "token": { "type": "string" }, "tokenId": { "type": "integer" }, "message": { "type": "string" } }, "required": [ "token", "tokenId", "message" ] }, "example": { "token": "***", "tokenId": 42, "message": "Token generated successfully. Save this token - it will not be shown again." } } } }, "400": { "description": "The name or environmentId field is missing or of the wrong type" }, "401": { "description": "Not authenticated" }, "403": { "description": "Admin access required" }, "500": { "description": "Failed to generate the token" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "environmentId": { "type": "integer" }, "expiresAt": { "type": "string" }, "rawToken": { "type": "string" } }, "required": [ "name", "environmentId" ] }, "example": { "name": "edge-01", "environmentId": 1, "expiresAt": "2027-01-01T00:00:00Z" } } } } }, "delete": { "operationId": "delete_api_hawser_tokens", "tags": [ "hawser" ], "summary": "Revoke a Hawser agent token by ID", "parameters": [ { "name": "id", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "ID of the token to revoke (from GET /api/hawser/tokens)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" } }, "required": [ "success", "message" ] }, "example": { "success": true, "message": "Token revoked" } } } }, "400": { "description": "The id query parameter is missing" }, "401": { "description": "Not authenticated" }, "403": { "description": "Admin access required" }, "500": { "description": "Failed to revoke the token" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/health": { "get": { "operationId": "get_api_health", "tags": [ "health" ], "summary": "Liveness probe — always returns 200 when the SvelteKit process is up", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string" }, "timestamp": { "type": "string" } }, "required": [ "status", "timestamp" ] }, "example": { "status": "ok", "timestamp": "2027-01-01T12:00:00.000Z" } } } } }, "security": [] } }, "/api/health/database": { "get": { "operationId": "get_api_health_database", "tags": [ "health" ], "summary": "Public database health check reporting schema/migration/table status; authenticated settings:view callers also get connection details", "parameters": [], "responses": { "200": { "description": "Schema health; an unhealthy database returns the same shape with HTTP 503", "content": { "application/json": { "schema": { "type": "object", "properties": { "healthy": { "type": "boolean" }, "database": { "type": "string" }, "migrationsTable": { "type": "boolean" }, "appliedMigrations": { "type": "integer" }, "pendingMigrations": { "type": "integer" }, "tables": { "type": "integer" }, "timestamp": { "type": "string" } }, "required": [ "healthy", "database", "migrationsTable", "appliedMigrations", "pendingMigrations", "tables", "timestamp" ] }, "example": { "healthy": true, "database": "sqlite", "migrationsTable": true, "appliedMigrations": 42, "pendingMigrations": 0, "tables": 25, "timestamp": "2026-07-01T10:00:00.000Z" } } } }, "500": { "description": "Unexpected error while checking database health" } }, "security": [], "description": "Returns HTTP 200 when healthy and 503 when unhealthy (schema drift or table loss); 500 on an unexpected error. Connection details and the migration tag are only included for authenticated callers with settings:view." } }, "/api/host": { "get": { "operationId": "get_api_host", "tags": [ "host" ], "summary": "Return host info (hostname, IP, CPU, memory, uptime, Docker counts) for the daemon behind an environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment to describe (basic local info is returned when omitted) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "hostname": { "type": "string" }, "ipAddress": { "type": "string" }, "platform": { "type": "string" }, "arch": { "type": "string" }, "cpus": { "type": "integer" }, "totalMemory": { "type": "integer" }, "freeMemory": { "type": "integer" }, "uptime": { "type": "integer" }, "dockerVersion": { "type": "string" }, "dockerContainers": { "type": "integer" }, "dockerContainersRunning": { "type": "integer" }, "dockerImages": { "type": "integer" }, "environment": { "type": "string" } }, "required": [ "hostname", "ipAddress", "platform", "arch", "cpus", "totalMemory", "freeMemory", "uptime", "dockerContainers", "dockerContainersRunning", "dockerImages" ] }, "example": { "hostname": "docker-host", "ipAddress": "192.168.1.10", "platform": "linux", "arch": "x64", "cpus": 8, "totalMemory": 16777216000, "freeMemory": 8388608000, "uptime": 123456, "dockerVersion": "27.0.3", "dockerContainers": 10, "dockerContainersRunning": 8, "dockerImages": 25, "environment": { "id": 1, "name": "local", "connectionType": "socket" } } } } }, "403": { "description": "Permission denied, or (enterprise) no access to this environment" }, "500": { "description": "Failed to get host info" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/icons/selfhst/{ref}": { "get": { "operationId": "get_api_icons_selfhst_ref", "tags": [ "icons" ], "summary": "Get a selfh.st app icon (raw SVG), fetched once from the CDN and cached on disk", "parameters": [ { "name": "ref", "in": "path", "required": true, "schema": { "type": "string" }, "description": "selfh.st icon Reference (lowercase letters, digits, hyphens), e.g. \"plex\"" } ], "responses": { "200": { "description": "Raw image/svg+xml body (the icon, or a neutral placeholder if it can't be resolved), Cache-Control public max-age=604800" }, "400": { "description": "The ref is not a valid selfh.st reference" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Proxies an icon from the selfh.st collection so the browser never contacts an external CDN directly. The first request for a given ref fetches the SVG from jsdelivr and caches it under DATA_DIR; later requests are served locally. Icons are CC BY 4.0 (selfh.st). When the icon can't be resolved it returns a neutral placeholder SVG (200) rather than a 404, so a grid of icons doesn't read as probing to a WAF." } }, "/api/icons/selfhst/batch": { "post": { "operationId": "post_api_icons_selfhst_batch", "tags": [ "icons" ], "summary": "Resolve multiple selfh.st app icons in one request (avoids per-icon requests a WAF flags as crawling)", "parameters": [], "responses": { "200": { "description": "icons maps each resolved reference to a data:image/svg+xml base64 URI; unresolvable refs are omitted.", "content": { "application/json": { "schema": { "type": "object", "properties": { "icons": { "type": "string" } }, "required": [ "icons" ] }, "example": { "icons": { "plex": "data:image/svg+xml;base64,PHN2Zy4uLg==" } } } } }, "400": { "description": "The request body is missing a refs array" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Takes a list of selfh.st icon references and returns each resolved icon as a data:image/svg+xml URI, keyed by reference. Unresolvable or invalid refs are omitted rather than returned as errors. Each icon is fetched from the CDN once and cached on disk, same as the single-icon endpoint.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "refs": { "type": "array", "items": { "type": "string" } } }, "required": [ "refs" ] }, "example": { "refs": [ "plex", "gitea", "grafana" ] } } } } } }, "/api/icons/selfhst-manifest": { "get": { "operationId": "get_api_icons_selfhst-manifest", "tags": [ "icons" ], "summary": "Get the selfh.st icon manifest (index.json), cached on disk with a 7-day TTL", "parameters": [], "responses": { "200": { "description": "The raw selfh.st index.json array (application/json)" }, "503": { "description": "The manifest is unavailable (never cached and the upstream fetch failed)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns the selfh.st collection manifest (~2880 entries with Name, Reference, format flags, Category, Tags) so the icon picker can search it. Fetched once from the CDN and cached under DATA_DIR. Returns 503 only if it has never been fetched and the fetch fails." } }, "/api/images": { "get": { "operationId": "get_api_images", "tags": [ "images" ], "summary": "List the Docker images of an environment (returns an empty array when no env is given or Docker is unreachable)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment whose images to list (from GET /api/environments)" } ], "responses": { "200": { "description": "Array of images (empty if no env is specified or the Docker connection fails)", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "Id": { "type": "string" }, "RepoTags": { "type": "array", "items": { "type": "string" } }, "Size": { "type": "integer" }, "Created": { "type": "integer" } }, "required": [ "Id" ] } }, "example": [ { "Id": "sha256:abc123", "RepoTags": [ "nginx:latest" ], "Size": 142000000, "Created": 1719830400 } ] } } }, "403": { "description": "Permission denied, or (enterprise) no access to this environment" }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/images/{id}": { "delete": { "operationId": "delete_api_images_id", "tags": [ "images" ], "summary": "Remove a Docker image by ID or name (optionally forced), scoped to an environment", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Image ID or name to remove (from GET /api/images)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment the image belongs to (from GET /api/environments)" }, { "name": "force", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Force removal even if the image is tagged or referenced (default false)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied, or (enterprise) no access to this environment" }, "409": { "description": "Image is in use by a running container or has dependent child images" }, "500": { "description": "Failed to remove image" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/images/{id}/export": { "get": { "operationId": "get_api_images_id_export", "tags": [ "images" ], "summary": "Export a Docker image as a downloadable tar (or tar.gz) stream", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Image ID or name to export (from GET /api/images)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment the image belongs to (from GET /api/environments)" }, { "name": "compress", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Gzip the tar stream and serve it as .tar.gz (default false)" } ], "responses": { "200": { "description": "The image tar (application/x-tar) or gzipped tar (application/gzip) as an attachment" }, "403": { "description": "Permission denied" }, "500": { "description": "Docker returned no response body, or the export failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/images/{id}/history": { "get": { "operationId": "get_api_images_id_history", "tags": [ "images" ], "summary": "Return the layer build history of a Docker image", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Image ID or name whose history to return (from GET /api/images)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment the image belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "Id": { "type": "string" }, "Created": { "type": "integer" }, "CreatedBy": { "type": "string" }, "Size": { "type": "integer" }, "Comment": { "type": "string" } } } }, "example": [ { "Id": "sha256:abc123", "Created": 1719830400, "CreatedBy": "/bin/sh -c #(nop) CMD", "Size": 0, "Comment": "" } ] } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to get image history" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/images/{id}/tag": { "post": { "operationId": "post_api_images_id_tag", "tags": [ "images" ], "summary": "Tag a Docker image into a repository (defaults the tag to \"latest\")", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Image ID or name to tag (from GET /api/images)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment the image belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Repository name is missing or not a string" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to tag image" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "repo": { "type": "string" }, "tag": { "type": "string" } }, "required": [ "repo" ] }, "example": { "repo": "registry.example.com/myapp", "tag": "v1.2.3" } } } } } }, "/api/images/load": { "post": { "operationId": "post_api_images_load", "tags": [ "images" ], "summary": "Load a Docker image from an uploaded tar (docker load) for air-gapped hosts", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Target environment id" } ], "responses": { "200": { "description": "loaded echoes the daemon's final line, e.g. \"Loaded image: alpine:3.20\"", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "loaded": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true, "loaded": "string" } } } }, "400": { "description": "Request body (an image tar) is required" }, "403": { "description": "Permission denied (needs images:load), or access denied to this environment" }, "500": { "description": "The daemon rejected the tar or the load failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "The request body is the raw image tar (Content-Type application/x-tar), streamed straight to the daemon without buffering. Local/socket or direct TCP only; Hawser is rejected.", "requestBody": { "required": true, "content": { "application/x-tar": { "schema": { "type": "string", "format": "binary", "description": "The raw image tar, streamed to the daemon (docker load)" } } } } } }, "/api/images/pull": { "post": { "operationId": "post_api_images_pull", "tags": [ "images" ], "summary": "Pull a Docker image and stream pull (and optional scan-on-pull) progress as Server-Sent Events", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment to pull into (from GET /api/environments)" } ], "responses": { "200": { "description": "A Server-Sent Events stream of pull progress, ending with a \"result\" event" }, "403": { "description": "Permission denied, or (enterprise) no access to this environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "image": { "type": "string" }, "scanAfterPull": { "type": "boolean" } }, "required": [ "image" ] }, "example": { "image": "nginx:latest", "scanAfterPull": false } } } } } }, "/api/images/push": { "post": { "operationId": "post_api_images_push", "tags": [ "images" ], "summary": "Tag a local image and push it to a configured registry (streams progress, or runs synchronously for Accept: application/json)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment the source image belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "jobId": { "type": "string" } } }, "example": { "jobId": "a1b2c3d4" } } } }, "400": { "description": "Image ID or registry ID missing, or the image has no usable tag" }, "403": { "description": "Permission denied" }, "404": { "description": "Registry not found" }, "500": { "description": "Failed to push image" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "imageId from GET /api/images. registryId from GET /api/registries.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "imageId": { "type": "string" }, "registryId": { "type": "integer" }, "imageName": { "type": "string" }, "newTag": { "type": "string" } }, "required": [ "imageId", "registryId" ] }, "example": { "imageId": "sha256:abc123", "registryId": 2, "imageName": "myapp:latest", "newTag": "myapp:v1.2.3" } } } } } }, "/api/images/scan": { "get": { "operationId": "get_api_images_scan", "tags": [ "images" ], "summary": "Return the latest cached vulnerability scan for an image, resolving its tag to a SHA256 ID", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment the image belongs to (from GET /api/environments)" }, { "name": "image", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Image name or ID to look up cached results for" }, { "name": "scanner", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Restrict the lookup to a specific scanner (e.g. grype or trivy)" } ], "responses": { "200": { "description": "found=false when no cached scan exists; otherwise result holds the stored scan", "content": { "application/json": { "schema": { "type": "object", "properties": { "found": { "type": "boolean" }, "result": { "type": "string" } }, "required": [ "found" ] }, "example": { "found": true, "result": { "imageId": "sha256:abc123", "scanner": "grype", "summary": { "critical": 0, "high": 2 } } } } } }, "400": { "description": "Image name is required" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to get scan results" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_images_scan", "tags": [ "images" ], "summary": "Start a vulnerability scan of an image and stream scan progress as Server-Sent Events, persisting the results", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment the image belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "A Server-Sent Events stream of scan progress, ending with a \"result\" event" }, "400": { "description": "Image name is required" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "imageName": { "type": "string" }, "scanner": { "type": "string" } }, "required": [ "imageName" ] }, "example": { "imageName": "nginx:latest", "scanner": "grype" } } } } } }, "/api/images/scan/export": { "get": { "operationId": "get_api_images_scan_export", "tags": [ "images" ], "summary": "Export the cached vulnerability findings for a single image as json, csv, or sarif", "parameters": [ { "name": "imageId", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Image SHA/ID to export (falls back to the \"image\" param when omitted) (from GET /api/images)" }, { "name": "image", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Image name/ID used as a fallback when imageId is not given" }, { "name": "format", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Output format — json (default), csv, or sarif" } ], "responses": { "200": { "description": "A 400 is returned when imageId is missing or (auth enabled) env is not specified, and 500 on failure" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/jobs/{id}": { "get": { "operationId": "get_api_jobs_id", "tags": [ "jobs" ], "summary": "Poll a background job's status and accumulated output lines (no auth — job ids are unguessable UUIDs)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Job id (UUID)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "status": { "type": "string" }, "lines": { "type": "array", "items": { "type": "object", "properties": { "event": { "type": "string" }, "data": { "type": "object", "properties": {} } } } }, "result": { "type": "object", "properties": {} } }, "required": [ "id", "status", "lines" ] }, "example": { "id": "string", "status": "string", "lines": [ { "event": "string", "data": {} } ], "result": {} } } } }, "404": { "description": "Job not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "operationId": "delete_api_jobs_id", "tags": [ "jobs" ], "summary": "Request cancellation of a running background job (no auth — job ids are unguessable UUIDs)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Job id (UUID)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "cancelled": { "type": "boolean" } }, "required": [ "cancelled" ] }, "example": { "cancelled": true } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/labels": { "get": { "operationId": "get_api_labels", "tags": [ "labels" ], "summary": "List all unique environment labels with their usage counts, the environments carrying each label, and any custom color", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "labels": { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string" }, "environments": { "type": "array", "items": { "type": "object", "properties": { "envId": { "type": "integer" }, "envName": { "type": "string" } }, "required": [ "envId", "envName" ] } }, "count": { "type": "integer" }, "color": { "type": "string" } }, "required": [ "label", "environments", "count" ] } }, "colors": { "type": "object", "properties": {} } }, "required": [ "labels" ] }, "example": { "labels": [ { "label": "prod", "environments": [ { "envId": 1, "envName": "Production" } ], "count": 1, "color": "#ff0000" } ], "colors": { "prod": "#ff0000" } } } } }, "403": { "description": "Permission denied (requires environments:view)" }, "500": { "description": "Failed to get labels" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_labels", "tags": [ "labels" ], "summary": "Perform a bulk label operation on environments — rename, delete, add to environments, or set a custom color — selected via the action field", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "affected": { "type": "integer" } }, "required": [ "success" ] }, "example": { "success": true, "affected": 2 } } } }, "400": { "description": "Invalid action or missing/empty required fields for the chosen action" }, "403": { "description": "Permission denied (requires environments:edit)" }, "500": { "description": "Failed to manage labels" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentIds from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "action": { "type": "string" }, "oldLabel": { "type": "string" }, "newLabel": { "type": "string" }, "label": { "type": "string" }, "environmentIds": { "type": "array", "items": { "type": "integer" } }, "color": { "type": "string" } }, "required": [ "action" ] }, "example": { "action": "rename", "oldLabel": "staging", "newLabel": "stg" } } } } } }, "/api/legal/license": { "get": { "operationId": "get_api_legal_license", "tags": [ "legal" ], "summary": "Return the bundled LICENSE.txt — as JSON by default, or as raw text/plain when format=text", "parameters": [ { "name": "format", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Set to \"text\" to return the raw license as text/plain instead of JSON" } ], "responses": { "200": { "description": "The license text (as {content} JSON, or raw text/plain when format=text)", "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" } }, "required": [ "content" ] }, "example": { "content": "string" } } } }, "404": { "description": "LICENSE.txt could not be found/read" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/legal/privacy": { "get": { "operationId": "get_api_legal_privacy", "tags": [ "legal" ], "summary": "Return the bundled PRIVACY.txt — as JSON by default, or as raw text/plain when format=text", "parameters": [ { "name": "format", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Set to \"text\" to return the raw privacy policy as text/plain instead of JSON" } ], "responses": { "200": { "description": "The privacy policy text (as {content} JSON, or raw text/plain when format=text)", "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" } }, "required": [ "content" ] }, "example": { "content": "string" } } } }, "404": { "description": "PRIVACY.txt could not be found/read" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/license": { "get": { "operationId": "get_api_license", "tags": [ "license" ], "summary": "Get the current license status plus the server hostname the license is bound to", "parameters": [], "responses": { "200": { "description": "The license status object merged with the current hostname" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "500": { "description": "Failed to read the license status" } }, "security": [] }, "post": { "operationId": "post_api_license", "tags": [ "license" ], "summary": "Activate a license by name and key", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "license": { "type": "object", "properties": {} } }, "required": [ "success" ] }, "example": { "success": true, "license": {} } } } }, "400": { "description": "Name and key are required, or activation was rejected (invalid key)" }, "403": { "description": "Permission denied (missing license:manage)" }, "500": { "description": "Failed to activate the license" } }, "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "key": { "type": "string" } }, "required": [ "name", "key" ] }, "example": { "name": "ACME Corp", "key": "***" } } } } }, "delete": { "operationId": "delete_api_license", "tags": [ "license" ], "summary": "Deactivate the currently active license", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (missing license:manage)" }, "500": { "description": "Failed to deactivate the license" } }, "security": [] } }, "/api/logs/merged": { "get": { "operationId": "get_api_logs_merged", "tags": [ "logs" ], "summary": "Stream merged, color-tagged logs from multiple containers over Server-Sent Events", "parameters": [ { "name": "containers", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Comma-separated container IDs to merge logs from" }, { "name": "tail", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Number of lines to tail per container (default 100)" }, { "name": "since", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only return logs since this timestamp (Docker since format)" }, { "name": "until", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only return logs until this timestamp (Docker until format)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the containers belong to (from GET /api/environments)" } ], "responses": { "200": { "description": "Server-Sent Events stream (text/event-stream) emitting heartbeat and per-container log events" }, "400": { "description": "No containers specified" }, "403": { "description": "Permission denied (requires the containers:logs permission)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/networks": { "get": { "operationId": "get_api_networks", "tags": [ "networks" ], "summary": "List Docker networks for an environment (returns an empty array when no env is given)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID to list networks from (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "Id": { "type": "string" }, "Name": { "type": "string" }, "Driver": { "type": "string" }, "Scope": { "type": "string" }, "Internal": { "type": "boolean" }, "Attachable": { "type": "boolean" } }, "required": [ "Id", "Name" ] } }, "example": [ { "Id": "string", "Name": "string", "Driver": "string", "Scope": "string", "Internal": true, "Attachable": true } ] } } }, "403": { "description": "Permission denied, or access denied to the requested environment (enterprise)" }, "404": { "description": "Environment not found" }, "500": { "description": "Failed to list networks" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_networks", "tags": [ "networks" ], "summary": "Create a Docker network in the given environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID to create the network in (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "id": { "type": "string" } }, "required": [ "success", "id" ] }, "example": { "success": true, "id": "3f1c...e9" } } } }, "400": { "description": "Network name is required" }, "403": { "description": "Permission denied, or access denied to the requested environment (enterprise)" }, "500": { "description": "Failed to create network" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "driver": { "type": "string" }, "internal": { "type": "boolean" }, "attachable": { "type": "boolean" }, "ingress": { "type": "boolean" }, "enableIPv6": { "type": "boolean" }, "options": { "type": "object", "properties": {} }, "labels": { "type": "object", "properties": {} }, "ipam": { "type": "object", "properties": { "driver": { "type": "string" }, "config": { "type": "array", "items": { "type": "object", "properties": {} } }, "options": { "type": "object", "properties": {} } } } }, "required": [ "name" ] }, "example": { "name": "app-net", "driver": "bridge", "attachable": true, "labels": { "project": "dockhand" } } } } } } }, "/api/networks/{id}": { "get": { "operationId": "get_api_networks_id", "tags": [ "networks" ], "summary": "Inspect a Docker network by ID (a malformed ID is rejected with 400 by input validation)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker network ID (from GET /api/networks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the network belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "Id": { "type": "string" }, "Name": { "type": "string" }, "Driver": { "type": "string" }, "Scope": { "type": "string" }, "IPAM": { "type": "object", "properties": {} }, "Containers": { "type": "object", "properties": {} } }, "required": [ "Id", "Name" ] }, "example": { "Id": "string", "Name": "string", "Driver": "string", "Scope": "string", "IPAM": {}, "Containers": {} } } } }, "403": { "description": "Permission denied, or access denied to the requested environment (enterprise)" }, "500": { "description": "Failed to inspect network" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "operationId": "delete_api_networks_id", "tags": [ "networks" ], "summary": "Remove a Docker network by ID (a malformed ID is rejected with 400 by input validation)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker network ID (from GET /api/networks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the network belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied, or access denied to the requested environment (enterprise)" }, "500": { "description": "Failed to remove network" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/networks/{id}/connect": { "post": { "operationId": "post_api_networks_id_connect", "tags": [ "networks" ], "summary": "Connect a container to a Docker network", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker network ID (from GET /api/networks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the network belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Container ID is required" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to connect container to network" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "containerId from GET /api/containers.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "containerId": { "type": "string" }, "containerName": { "type": "string" } }, "required": [ "containerId" ] }, "example": { "containerId": "a1b2c3d4e5f6", "containerName": "web-1" } } } } } }, "/api/networks/{id}/disconnect": { "post": { "operationId": "post_api_networks_id_disconnect", "tags": [ "networks" ], "summary": "Disconnect a container from a Docker network (optionally forcing the disconnect)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker network ID (from GET /api/networks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the network belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Container ID is required" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to disconnect container from network" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "containerId from GET /api/containers.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "containerId": { "type": "string" }, "containerName": { "type": "string" }, "force": { "type": "boolean" } }, "required": [ "containerId" ] }, "example": { "containerId": "a1b2c3d4e5f6", "containerName": "web-1", "force": false } } } } } }, "/api/networks/{id}/inspect": { "get": { "operationId": "get_api_networks_id_inspect", "tags": [ "networks" ], "summary": "Inspect a Docker network by ID (a malformed ID is rejected with 400 by input validation)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker network ID (from GET /api/networks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment the network belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "Id": { "type": "string" }, "Name": { "type": "string" }, "Driver": { "type": "string" }, "Scope": { "type": "string" }, "IPAM": { "type": "object", "properties": {} }, "Containers": { "type": "object", "properties": {} } }, "required": [ "Id", "Name" ] }, "example": { "Id": "string", "Name": "string", "Driver": "string", "Scope": "string", "IPAM": {}, "Containers": {} } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to inspect network" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/notifications": { "get": { "operationId": "get_api_notifications", "tags": [ "notifications" ], "summary": "List all notification settings (SMTP and Apprise) with SMTP passwords masked", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "type": { "type": "string" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "config": { "type": "object", "properties": { "host": { "type": "string" }, "port": { "type": "integer" }, "from_email": { "type": "string" }, "to_emails": { "type": "array", "items": { "type": "string" } }, "urls": { "type": "array", "items": { "type": "string" } }, "password": { "type": "string" } } }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "id", "type", "name", "enabled" ] } }, "example": [ { "id": 0, "type": "string", "name": "string", "enabled": true, "config": { "host": "string", "port": 0, "from_email": "string", "to_emails": [ "string" ], "urls": [ "string" ], "password": "string" }, "eventTypes": [ "string" ] } ] } } }, "403": { "description": "Permission denied (requires the notifications:view permission when auth is enabled)" }, "500": { "description": "Failed to fetch notification settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_notifications", "tags": [ "notifications" ], "summary": "Create a notification setting (SMTP or Apprise); the response masks any SMTP password", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "type": { "type": "string" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "config": { "type": "object", "properties": {} }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "id", "type", "name", "enabled" ] }, "example": { "id": 0, "type": "string", "name": "string", "enabled": true, "config": {}, "eventTypes": [ "string" ] } } } }, "400": { "description": "Missing or invalid fields — type/name/config required, type must be smtp or apprise, SMTP needs host/port/from_email/to_emails, Apprise needs at least one URL" }, "403": { "description": "Permission denied (requires the notifications:create permission)" }, "500": { "description": "Failed to create notification setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "type": { "type": "string" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "config": { "type": "object", "properties": { "host": { "type": "string" }, "port": { "type": "integer" }, "secure": { "type": "boolean" }, "username": { "type": "string" }, "password": { "type": "string" }, "from_email": { "type": "string" }, "from_name": { "type": "string" }, "to_emails": { "type": "array", "items": { "type": "string" } }, "urls": { "type": "array", "items": { "type": "string" } } } }, "eventTypes": { "type": "array", "items": { "type": "string" } }, "event_types": { "type": "array", "items": { "type": "string" } } }, "required": [ "type", "name" ] }, "example": { "type": "smtp", "name": "Ops Alerts", "enabled": true, "config": { "host": "smtp.example.com", "port": 587, "secure": false, "from_email": "dockhand@example.com", "to_emails": [ "ops@example.com" ], "password": "***" }, "eventTypes": [ "container_unhealthy" ] } } } } } }, "/api/notifications/{id}": { "get": { "operationId": "get_api_notifications_id", "tags": [ "notifications" ], "summary": "Get a single notification setting by ID, with any SMTP password masked", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Notification setting ID (from GET /api/notifications)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "type": { "type": "string" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "config": { "type": "object", "properties": { "host": { "type": "string" }, "port": { "type": "integer" }, "from_email": { "type": "string" }, "to_emails": { "type": "array", "items": { "type": "string" } }, "urls": { "type": "array", "items": { "type": "string" } }, "password": { "type": "string" } } }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "id", "type", "name", "enabled" ] }, "example": { "id": 0, "type": "string", "name": "string", "enabled": true, "config": { "host": "string", "port": 0, "from_email": "string", "to_emails": [ "string" ], "urls": [ "string" ], "password": "string" }, "eventTypes": [ "string" ] } } } }, "400": { "description": "Invalid ID (not a number)" }, "403": { "description": "Permission denied (requires the notifications:view permission)" }, "404": { "description": "Notification setting not found" }, "500": { "description": "Failed to fetch notification setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_notifications_id", "tags": [ "notifications" ], "summary": "Update a notification setting; a masked SMTP password (\"********\") keeps the stored value", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Notification setting ID (from GET /api/notifications)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "type": { "type": "string" }, "name": { "type": "string" }, "enabled": { "type": "boolean" }, "config": { "type": "object", "properties": {} }, "eventTypes": { "type": "array", "items": { "type": "string" } } }, "required": [ "id", "type", "name", "enabled" ] }, "example": { "id": 0, "type": "string", "name": "string", "enabled": true, "config": {}, "eventTypes": [ "string" ] } } } }, "400": { "description": "Invalid ID, or config validation failed (SMTP needs host/port/from_email/to_emails, Apprise needs at least one URL)" }, "403": { "description": "Permission denied (requires the notifications:edit permission)" }, "404": { "description": "Notification setting not found" }, "500": { "description": "Failed to update notification setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "enabled": { "type": "boolean" }, "config": { "type": "object", "properties": { "host": { "type": "string" }, "port": { "type": "integer" }, "secure": { "type": "boolean" }, "username": { "type": "string" }, "password": { "type": "string" }, "from_email": { "type": "string" }, "from_name": { "type": "string" }, "to_emails": { "type": "array", "items": { "type": "string" } }, "urls": { "type": "array", "items": { "type": "string" } } } }, "eventTypes": { "type": "array", "items": { "type": "string" } }, "event_types": { "type": "array", "items": { "type": "string" } } } }, "example": { "name": "Ops Alerts", "enabled": false, "config": { "host": "smtp.example.com", "port": 587, "from_email": "dockhand@example.com", "to_emails": [ "ops@example.com" ], "password": "********" }, "eventTypes": [ "container_unhealthy", "container_oom" ] } } } } }, "delete": { "operationId": "delete_api_notifications_id", "tags": [ "notifications" ], "summary": "Delete a notification setting by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Notification setting ID (from GET /api/notifications)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid ID (not a number)" }, "403": { "description": "Permission denied (requires the notifications:delete permission)" }, "404": { "description": "Notification setting not found" }, "500": { "description": "Failed to delete notification setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/notifications/{id}/test": { "post": { "operationId": "post_api_notifications_id_test", "tags": [ "notifications" ], "summary": "Send a test notification through an already-saved notification setting", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Notification setting ID (from GET /api/notifications)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true, "message": "Test notification sent successfully" } } } }, "400": { "description": "Invalid ID (not a number)" }, "404": { "description": "Notification setting not found" }, "500": { "description": "Failed to test notification" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/notifications/test": { "post": { "operationId": "post_api_notifications_test", "tags": [ "notifications" ], "summary": "Send a test notification using an ad-hoc config supplied in the body (nothing is saved)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" }, "error": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true, "message": "Test notification sent successfully" } } } }, "400": { "description": "Missing/invalid fields — type and config required; SMTP needs host/from_email/to_emails; Apprise needs at least one URL" }, "403": { "description": "Permission denied (requires the settings:edit permission)" }, "500": { "description": "Failed to test notification" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "type": { "type": "string" }, "name": { "type": "string" }, "config": { "type": "object", "properties": { "host": { "type": "string" }, "port": { "type": "integer" }, "secure": { "type": "boolean" }, "username": { "type": "string" }, "password": { "type": "string" }, "from_email": { "type": "string" }, "from_name": { "type": "string" }, "to_emails": { "type": "array", "items": { "type": "string" } }, "urls": { "type": "array", "items": { "type": "string" } } } } }, "required": [ "type" ] }, "example": { "type": "smtp", "name": "Test", "config": { "host": "smtp.example.com", "port": 587, "from_email": "dockhand@example.com", "to_emails": [ "ops@example.com" ], "password": "***" } } } } } } }, "/api/notifications/trigger-test": { "get": { "operationId": "get_api_notifications_trigger-test", "tags": [ "notifications" ], "summary": "List all available notification event types, grouped into categories", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "eventTypes": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" } }, "required": [ "id" ] } }, "categories": { "type": "object", "properties": { "container": { "type": "array", "items": { "type": "string" } }, "autoUpdate": { "type": "array", "items": { "type": "string" } }, "gitStack": { "type": "array", "items": { "type": "string" } }, "stack": { "type": "array", "items": { "type": "string" } }, "security": { "type": "array", "items": { "type": "string" } }, "system": { "type": "array", "items": { "type": "string" } } } } } }, "example": { "eventTypes": [ { "id": "string", "label": "string" } ], "categories": { "container": [ "string" ], "autoUpdate": [ "string" ], "gitStack": [ "string" ], "stack": [ "string" ], "security": [ "string" ], "system": [ "string" ] } } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_notifications_trigger-test", "tags": [ "notifications" ], "summary": "Trigger a real notification for a given event type (development/testing helper)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "sent": { "type": "integer" }, "eventType": { "type": "string" }, "environmentId": { "type": "integer" } }, "required": [ "success", "eventType" ] }, "example": { "success": true, "sent": 1, "eventType": "container_unhealthy", "environmentId": 1 } } } }, "400": { "description": "eventType required, payload with title and message required, unknown event type, or environmentId missing for a non-system event" }, "500": { "description": "Unknown error while sending the notification" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "eventType": { "type": "string" }, "environmentId": { "type": "integer" }, "payload": { "type": "object", "properties": { "title": { "type": "string" }, "message": { "type": "string" }, "type": { "type": "string" } }, "required": [ "title", "message" ] } }, "required": [ "eventType" ] }, "example": { "eventType": "container_unhealthy", "environmentId": 1, "payload": { "title": "Container unhealthy", "message": "web-1 is unhealthy", "type": "warning" } } } } } } }, "/api/preferences/favorite-groups": { "get": { "operationId": "get_api_preferences_favorite-groups", "tags": [ "preferences" ], "summary": "Get the saved log favorite-groups for an environment", "parameters": [ { "name": "env", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "Environment ID (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "groups": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "containers": { "type": "array", "items": { "type": "string" } } }, "required": [ "name" ] } } } }, "example": { "groups": [ { "name": "frontend", "containers": [ "web-1", "web-2" ] } ] } } } }, "400": { "description": "Environment ID is required, or invalid (not a number)" }, "500": { "description": "Failed to get favorite groups" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_preferences_favorite-groups", "tags": [ "preferences" ], "summary": "Add, remove, update or reorder log favorite-groups for an environment", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "groups": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "containers": { "type": "array", "items": { "type": "string" } } }, "required": [ "name" ] } } } }, "example": { "groups": [ { "name": "string", "containers": [ "string" ] } ] } } } }, "400": { "description": "Invalid environmentId, unknown action, missing group name, empty/missing containers, duplicate group name, or missing groups array for reorder" }, "404": { "description": "Group not found (update action)" }, "500": { "description": "Failed to update favorite groups" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "environmentId": { "type": "integer" }, "action": { "type": "string" }, "name": { "type": "string" }, "containers": { "type": "array", "items": { "type": "string" } }, "newName": { "type": "string" }, "groups": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "containers": { "type": "array", "items": { "type": "string" } } }, "required": [ "name" ] } } }, "required": [ "environmentId", "action" ] }, "example": { "environmentId": 1, "action": "add", "name": "frontend", "containers": [ "web-1", "web-2" ] } } } } } }, "/api/preferences/favorites": { "get": { "operationId": "get_api_preferences_favorites", "tags": [ "preferences" ], "summary": "Get the saved log favorites (container names) for an environment", "parameters": [ { "name": "env", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "Environment ID (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "favorites": { "type": "array", "items": { "type": "string" } } } }, "example": { "favorites": [ "web-1", "db-1" ] } } } }, "400": { "description": "Environment ID is required, or invalid (not a number)" }, "500": { "description": "Failed to get favorites" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_preferences_favorites", "tags": [ "preferences" ], "summary": "Add, remove or reorder log favorites (container names) for an environment", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "favorites": { "type": "array", "items": { "type": "string" } } } }, "example": { "favorites": [ "string" ] } } } }, "400": { "description": "Invalid environmentId, unknown action, missing favorites array for reorder, or missing containerName for add/remove" }, "500": { "description": "Failed to update favorites" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "environmentId": { "type": "integer" }, "action": { "type": "string" }, "containerName": { "type": "string" }, "favorites": { "type": "array", "items": { "type": "string" } } }, "required": [ "environmentId", "action" ] }, "example": { "environmentId": 1, "action": "add", "containerName": "web-1" } } } } } }, "/api/preferences/grid": { "get": { "operationId": "get_api_preferences_grid", "tags": [ "preferences" ], "summary": "Retrieve all saved data-grid column preferences (per-user when auth is enabled)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "preferences": { "type": "object", "properties": {} } } }, "example": { "preferences": { "containers": { "columns": [ { "id": "name", "visible": true } ] } } } } } }, "500": { "description": "Failed to get grid preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_preferences_grid", "tags": [ "preferences" ], "summary": "Save column preferences for one data grid and return all grid preferences", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "preferences": { "type": "object", "properties": {} } } }, "example": { "preferences": {} } } } }, "400": { "description": "gridId is required, columns array is required, or a column is missing id (string) / visible (boolean)" }, "500": { "description": "Failed to save grid preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "gridId": { "type": "string" }, "columns": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "visible": { "type": "boolean" } }, "required": [ "id", "visible" ] } } }, "required": [ "gridId" ] }, "example": { "gridId": "containers", "columns": [ { "id": "name", "visible": true }, { "id": "image", "visible": false } ] } } } } }, "delete": { "operationId": "delete_api_preferences_grid", "tags": [ "preferences" ], "summary": "Reset grid preferences — a single grid when gridId is given, otherwise all grids", "parameters": [ { "name": "gridId", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Grid ID to reset; omit to reset every grid" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "preferences": { "type": "object", "properties": {} } } }, "example": { "preferences": {} } } } }, "500": { "description": "Failed to reset grid preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/preferences/sidebar": { "get": { "operationId": "get_api_preferences_sidebar", "tags": [ "preferences" ], "summary": "Retrieve the saved sidebar menu preferences (order and hidden items)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "preferences": { "type": "object", "properties": { "order": { "type": "array", "items": { "type": "string" } }, "hidden": { "type": "array", "items": { "type": "string" } } } } } }, "example": { "preferences": { "order": [ "dashboard", "containers" ], "hidden": [ "volumes" ] } } } } }, "500": { "description": "Failed to get sidebar preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_preferences_sidebar", "tags": [ "preferences" ], "summary": "Save the sidebar menu preferences (order and hidden items)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "preferences": { "type": "object", "properties": { "order": { "type": "array", "items": { "type": "string" } }, "hidden": { "type": "array", "items": { "type": "string" } } } } } }, "example": { "preferences": { "order": [ "string" ], "hidden": [ "string" ] } } } } }, "400": { "description": "order must be an array of strings, or hidden must be an array of strings" }, "500": { "description": "Failed to save sidebar preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "order": { "type": "array", "items": { "type": "string" } }, "hidden": { "type": "array", "items": { "type": "string" } } }, "required": [ "order", "hidden" ] }, "example": { "order": [ "dashboard", "containers", "stacks" ], "hidden": [ "volumes" ] } } } } }, "delete": { "operationId": "delete_api_preferences_sidebar", "tags": [ "preferences" ], "summary": "Reset the sidebar menu preferences to their default order/visibility", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "preferences": { "type": "object", "properties": { "order": { "type": "array", "items": { "type": "string" } }, "hidden": { "type": "array", "items": { "type": "string" } } } } } }, "example": { "preferences": { "order": [ "string" ], "hidden": [ "string" ] } } } } }, "500": { "description": "Failed to reset sidebar preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/profile": { "get": { "operationId": "get_api_profile", "tags": [ "profile" ], "summary": "Get the authenticated user's own profile", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "avatar": { "type": "string" }, "mfaEnabled": { "type": "boolean" }, "isAdmin": { "type": "boolean" }, "provider": { "type": "string" }, "lastLogin": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "username", "mfaEnabled", "isAdmin", "provider", "createdAt", "updatedAt" ] }, "example": { "id": 0, "username": "string", "email": "string", "displayName": "string", "avatar": "string", "mfaEnabled": true, "isAdmin": true, "provider": "string", "lastLogin": "string", "createdAt": "string", "updatedAt": "string" } } } }, "400": { "description": "Authentication is not enabled" }, "401": { "description": "Not authenticated" }, "404": { "description": "User not found" }, "500": { "description": "Failed to read the profile" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_profile", "tags": [ "profile" ], "summary": "Update the authenticated user's own profile (email/display name, and optionally the password with current-password confirmation)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "avatar": { "type": "string" }, "mfaEnabled": { "type": "boolean" }, "isAdmin": { "type": "boolean" }, "lastLogin": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "username", "mfaEnabled", "isAdmin", "createdAt", "updatedAt" ] }, "example": { "id": 0, "username": "string", "email": "string", "displayName": "string", "avatar": "string", "mfaEnabled": true, "isAdmin": true, "lastLogin": "string", "createdAt": "string", "updatedAt": "string" } } } }, "400": { "description": "Authentication is not enabled, current password missing, new password too short (<8), or current password incorrect" }, "401": { "description": "Not authenticated" }, "404": { "description": "User not found" }, "500": { "description": "Failed to update the profile" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "email": { "type": "string" }, "displayName": { "type": "string" }, "currentPassword": { "type": "string" }, "newPassword": { "type": "string" } } }, "example": { "displayName": "Jane Doe", "currentPassword": "***", "newPassword": "***" } } } } } }, "/api/profile/avatar": { "post": { "operationId": "post_api_profile_avatar", "tags": [ "profile" ], "summary": "Upload the authenticated user's avatar as a base64 image data URL (max ~500KB)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "avatar": { "type": "string" } }, "required": [ "success", "avatar" ] }, "example": { "success": true, "avatar": "string" } } } }, "400": { "description": "Authentication is not enabled, avatar data missing, not an image data URL, or image too large (>500KB)" }, "401": { "description": "Not authenticated" }, "500": { "description": "Failed to upload the avatar" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "avatar": { "type": "string" } }, "required": [ "avatar" ] }, "example": { "avatar": "data:image/png;base64,iVBORw0KGgo..." } } } } }, "delete": { "operationId": "delete_api_profile_avatar", "tags": [ "profile" ], "summary": "Remove the authenticated user's avatar", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Authentication is not enabled" }, "401": { "description": "Not authenticated" }, "500": { "description": "Failed to remove the avatar" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/profile/preferences": { "get": { "operationId": "get_api_profile_preferences", "tags": [ "profile" ], "summary": "Get the current user's UI preferences (theme, fonts, editor options)", "parameters": [], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Not authenticated / no user in context" }, "401": { "description": "Not authenticated" }, "500": { "description": "Failed to load preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_profile_preferences", "tags": [ "profile" ], "summary": "Update the current user's UI preferences (each field is optional)", "parameters": [], "responses": { "200": { "description": "Successful response" }, "400": { "description": "A supplied field has the wrong type (e.g. editorIndentGuides not a boolean)" }, "401": { "description": "Not authenticated" }, "500": { "description": "Failed to save preferences" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "lightTheme": { "type": "string" }, "darkTheme": { "type": "string" }, "font": { "type": "string" }, "fontSize": { "type": "string" }, "gridFontSize": { "type": "string" }, "terminalFont": { "type": "string" }, "editorFont": { "type": "string" }, "animateIcons": { "type": "boolean" }, "coloredActionButtons": { "type": "boolean" }, "actionIconSize": { "type": "string" }, "editorIndentGuides": { "type": "boolean" } } }, "example": { "lightTheme": "string", "darkTheme": "string", "font": "string", "fontSize": "string", "gridFontSize": "string", "terminalFont": "string", "editorFont": "string", "animateIcons": true, "coloredActionButtons": true, "actionIconSize": "string", "editorIndentGuides": true } } } } } }, "/api/prune/all": { "post": { "operationId": "post_api_prune_all", "tags": [ "prune" ], "summary": "Prune all unused Docker resources (containers, images, volumes and networks) in a single operation", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Target environment id; scopes both the prune operation and the permission check (defaults to the local environment) (from GET /api/environments)" } ], "responses": { "200": { "description": "Returns { success: true, result } where result is the aggregated Docker prune report (space reclaimed, items deleted)" }, "403": { "description": "Permission denied — requires the \"remove\" permission on containers, images, volumes AND networks for the target environment" }, "500": { "description": "Failed to prune the system (Docker error); the message is returned in \"details\"" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/prune/containers": { "post": { "operationId": "post_api_prune_containers", "tags": [ "prune" ], "summary": "Prune (delete) all stopped containers in the target environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Target environment id; scopes both the prune operation and the permission check (defaults to the local environment) (from GET /api/environments)" } ], "responses": { "200": { "description": "Returns { success: true, result } where result is the Docker container-prune report (deleted container ids, space reclaimed)" }, "403": { "description": "Permission denied — requires the \"remove\" permission on containers for the target environment" }, "500": { "description": "Failed to prune containers (Docker error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/prune/images": { "post": { "operationId": "post_api_prune_images", "tags": [ "prune" ], "summary": "Prune unused Docker images, streaming progress as a Server-Sent Events job", "parameters": [ { "name": "dangling", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "When not \"false\", prune only dangling images; set \"dangling=false\" to prune all unused images (defaults to dangling-only)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Target environment id; scopes both the prune operation and the permission check (defaults to the local environment) (from GET /api/environments)" } ], "responses": { "200": { "description": "Server-Sent Events stream; the final `result` event contains { success, result } or { success:false, error }" }, "403": { "description": "Permission denied — requires the \"remove\" permission on images for the target environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Returns a text/event-stream. On completion a `result` event carries { success, result } on success or { success:false, error } on failure — the operation itself never returns a non-200 HTTP status once the permission check passes." } }, "/api/prune/networks": { "post": { "operationId": "post_api_prune_networks", "tags": [ "prune" ], "summary": "Prune (delete) all unused Docker networks in the target environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Target environment id; scopes both the prune operation and the permission check (defaults to the local environment) (from GET /api/environments)" } ], "responses": { "200": { "description": "Returns { success: true, result } where result is the Docker network-prune report (deleted network names)" }, "403": { "description": "Permission denied — requires the \"remove\" permission on networks for the target environment" }, "500": { "description": "Failed to prune networks (Docker error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/prune/volumes": { "post": { "operationId": "post_api_prune_volumes", "tags": [ "prune" ], "summary": "Prune (delete) all unused Docker volumes in the target environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Target environment id; scopes both the prune operation and the permission check (defaults to the local environment) (from GET /api/environments)" } ], "responses": { "200": { "description": "Returns { success: true, result } where result is the Docker volume-prune report (deleted volume names, space reclaimed)" }, "403": { "description": "Permission denied — requires the \"remove\" permission on volumes for the target environment" }, "500": { "description": "Failed to prune volumes (Docker error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/registries": { "get": { "operationId": "get_api_registries", "tags": [ "registries" ], "summary": "List all configured container registries with passwords stripped (only a hasCredentials flag)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "isDefault": { "type": "boolean" }, "hasCredentials": { "type": "boolean" } }, "required": [ "id", "name", "url", "hasCredentials" ] } }, "example": [ { "id": 1, "name": "Docker Hub", "url": "https://docker.io", "isDefault": true, "hasCredentials": false } ] } } }, "403": { "description": "Caller lacks the registries:view permission" }, "500": { "description": "Failed to read registries" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_registries", "tags": [ "registries" ], "summary": "Create a container registry (optionally set it as default); credentials are trimmed and stored encrypted", "parameters": [], "responses": { "201": { "description": "Registry created", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "isDefault": { "type": "boolean" }, "hasCredentials": { "type": "boolean" } }, "required": [ "id", "name", "url", "hasCredentials" ] }, "example": { "id": 0, "name": "string", "url": "string", "isDefault": true, "hasCredentials": true } } } }, "400": { "description": "Missing name or url, or a duplicate registry name" }, "403": { "description": "Caller lacks the registries:create permission" }, "500": { "description": "Failed to create the registry" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string" }, "username": { "type": "string" }, "password": { "type": "string" }, "isDefault": { "type": "boolean" } }, "required": [ "name", "url" ] }, "example": { "name": "GHCR", "url": "https://ghcr.io", "username": "deploy", "password": "***", "isDefault": false } } } } } }, "/api/registries/{id}": { "get": { "operationId": "get_api_registries_id", "tags": [ "registries" ], "summary": "Get a single registry by ID with the password stripped (only a hasCredentials flag)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Registry ID (from GET /api/registries)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "isDefault": { "type": "boolean" }, "hasCredentials": { "type": "boolean" } }, "required": [ "id", "name", "url", "hasCredentials" ] }, "example": { "id": 0, "name": "string", "url": "string", "isDefault": true, "hasCredentials": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the registries:view permission" }, "404": { "description": "No registry exists with that ID" }, "500": { "description": "Failed to read the registry" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_registries_id", "tags": [ "registries" ], "summary": "Update a registry (optionally set as default); credentials are trimmed and the response strips the password", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Registry ID (from GET /api/registries)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "isDefault": { "type": "boolean" }, "hasCredentials": { "type": "boolean" } }, "required": [ "id", "name", "url", "hasCredentials" ] }, "example": { "id": 0, "name": "string", "url": "string", "isDefault": true, "hasCredentials": true } } } }, "400": { "description": "Invalid id, or a duplicate registry name" }, "403": { "description": "Caller lacks the registries:edit permission" }, "404": { "description": "No registry exists with that ID" }, "500": { "description": "The update failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string" }, "username": { "type": "string" }, "password": { "type": "string" }, "isDefault": { "type": "boolean" } } }, "example": { "name": "GHCR", "url": "https://ghcr.io", "username": "deploy", "password": "***", "isDefault": true } } } } }, "delete": { "operationId": "delete_api_registries_id", "tags": [ "registries" ], "summary": "Delete a registry by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Registry ID (from GET /api/registries)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid id, or the registry cannot be deleted" }, "403": { "description": "Caller lacks the registries:delete permission" }, "404": { "description": "No registry exists with that ID" }, "500": { "description": "The deletion failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/registries/{id}/default": { "post": { "operationId": "post_api_registries_id_default", "tags": [ "registries" ], "summary": "Mark a registry as the default registry", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Registry ID (from GET /api/registries)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "The id path segment is not a valid integer" }, "403": { "description": "Caller lacks the settings:edit permission" }, "404": { "description": "No registry exists with that ID" }, "500": { "description": "Failed to set the default registry" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/registries/test": { "post": { "operationId": "post_api_registries_test", "tags": [ "registries" ], "summary": "Test registry connectivity and (if credentials are given) authentication against the V2 endpoint", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "connectivity": { "type": "boolean" }, "authenticated": { "type": "boolean" }, "message": { "type": "string" } }, "required": [ "success", "connectivity", "message" ] }, "example": { "success": true, "connectivity": true, "authenticated": true, "message": "Connected and authenticated as deploy" } } } }, "400": { "description": "The url field is missing (and no registryId was supplied)" }, "403": { "description": "Caller lacks the registries:view permission" }, "404": { "description": "The referenced registryId does not exist" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Pass either a saved registryId or inline url/username/password. The outcome is always reported in a 200 body via the success/connectivity/authenticated fields, never as an HTTP error. registryId from GET /api/registries.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "registryId": { "type": "integer" }, "url": { "type": "string" }, "username": { "type": "string" }, "password": { "type": "string" } } }, "example": { "url": "https://ghcr.io", "username": "deploy", "password": "***" } } } } } }, "/api/registry/catalog": { "get": { "operationId": "get_api_registry_catalog", "tags": [ "registry" ], "summary": "List repositories in a registry's V2 catalog (with Harbor project-API fallback), paginated", "parameters": [ { "name": "registry", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "ID of the configured registry to query (from GET /api/registries)" }, { "name": "last", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Opaque pagination cursor from a previous page's nextLast" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "repositories": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "star_count": { "type": "integer" }, "is_official": { "type": "boolean" }, "is_automated": { "type": "boolean" } }, "required": [ "name" ] } }, "pagination": { "type": "object", "properties": { "pageSize": { "type": "integer" }, "hasMore": { "type": "boolean" }, "nextLast": { "type": "string" } }, "required": [ "pageSize", "hasMore" ] } }, "required": [ "repositories", "pagination" ] }, "example": { "repositories": [ { "name": "library/nginx", "description": "", "star_count": 0, "is_official": false, "is_automated": false } ], "pagination": { "pageSize": 100, "hasMore": false, "nextLast": null } } } } }, "400": { "description": "Missing registry parameter, or Docker Hub was targeted (catalog listing unsupported)" }, "404": { "description": "Registry not found, or the registry does not implement the V2 catalog API" }, "500": { "description": "Failed to fetch the catalog" }, "503": { "description": "Could not connect to the registry (connection refused, host not found, or a TLS error)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Docker Hub is rejected (no catalog API). For 401/403/404 from the upstream registry the same status is proxied back to the caller." } }, "/api/registry/image": { "delete": { "operationId": "delete_api_registry_image", "tags": [ "registry" ], "summary": "Delete a tagged image from a registry by resolving its manifest digest, then deleting by digest", "parameters": [ { "name": "registry", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "ID of the configured registry (from GET /api/registries)" }, { "name": "image", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Repository/image name (may include an org path)" }, { "name": "tag", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Tag to delete" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" } }, "required": [ "success" ] }, "example": { "success": true, "message": "Deleted library/nginx:1.27" } } } }, "400": { "description": "A required query parameter is missing, or Docker Hub was targeted (deletion unsupported)" }, "401": { "description": "The registry rejected authentication while resolving or deleting the manifest" }, "403": { "description": "Caller lacks the settings:edit permission" }, "404": { "description": "Registry not found, or the image/tag/manifest does not exist" }, "405": { "description": "The registry has manifest deletion disabled (REGISTRY_STORAGE_DELETE_ENABLED)" }, "500": { "description": "Failed to delete the image" }, "503": { "description": "Could not connect to the registry (connection refused or host not found)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Docker Hub deletion is rejected. Upstream 401 responses during manifest resolution/deletion are proxied back as 401." } }, "/api/registry/search": { "get": { "operationId": "get_api_registry_search", "tags": [ "registry" ], "summary": "Search for images by name in Docker Hub or a configured private registry (direct + catalog fallback)", "parameters": [ { "name": "term", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Search term / image name" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Maximum number of results (default 25)" }, { "name": "registry", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the configured registry to search; omit to search Docker Hub (from GET /api/registries)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "star_count": { "type": "integer" }, "is_official": { "type": "boolean" }, "is_automated": { "type": "boolean" } }, "required": [ "name" ] } }, "example": [ { "name": "nginx", "description": "Official build of Nginx", "star_count": 20000, "is_official": true, "is_automated": false } ] } } }, "400": { "description": "The term query parameter is missing" }, "404": { "description": "The referenced registry does not exist" }, "500": { "description": "Failed to search images" }, "503": { "description": "Could not connect to the registry (connection refused or host not found)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "With no registry parameter the search runs against Docker Hub; otherwise against the given registry, falling back to Docker Hub search when the registry is a Docker Hub mirror." } }, "/api/registry/tag-info": { "get": { "operationId": "get_api_registry_tag-info", "tags": [ "registry" ], "summary": "Resolve the compressed size and creation date of a single registry tag from its manifest (lazy per-tag fetch for the registry browser)", "parameters": [ { "name": "registry", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Registry ID; omitted or Docker Hub registries return a graceful \"not supported\" result instead of a manifest fetch (from GET /api/registries)" }, { "name": "image", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Image repository/name" }, { "name": "tag", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Tag name or digest" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "size": { "type": "number" }, "lastUpdated": { "type": "string" }, "reason": { "type": "string" } } }, "example": { "size": 104857600, "lastUpdated": "2026-06-01T12:00:00Z" } } } }, "400": { "description": "image and tag are required" }, "404": { "description": "Registry not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/registry/tags": { "get": { "operationId": "get_api_registry_tags", "tags": [ "registry" ], "summary": "List the tags of an image from Docker Hub or a configured registry (paginated for Docker Hub)", "parameters": [ { "name": "registry", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the configured registry; omit to query Docker Hub (from GET /api/registries)" }, { "name": "image", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Repository/image name" }, { "name": "page", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Page number for Docker Hub pagination (default 1)" }, { "name": "pageSize", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Page size for Docker Hub pagination (default 20)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "size": { "type": "integer" }, "lastUpdated": { "type": "string" }, "digest": { "type": "string" } }, "required": [ "name" ] } }, "total": { "type": "integer" }, "page": { "type": "integer" }, "pageSize": { "type": "integer" }, "hasNext": { "type": "boolean" }, "hasPrev": { "type": "boolean" } }, "required": [ "tags", "total", "page", "pageSize", "hasNext", "hasPrev" ] }, "example": { "tags": [ { "name": "1.27", "lastUpdated": "2026-06-01T00:00:00Z" } ], "total": 1, "page": 1, "pageSize": 20, "hasNext": false, "hasPrev": false } } } }, "400": { "description": "The image query parameter is missing" }, "404": { "description": "The referenced registry does not exist" }, "500": { "description": "Failed to fetch tags" }, "503": { "description": "Could not connect to the registry (connection refused or host not found)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "With no registry parameter, Docker Hub is queried; V2 registries return all tags in one page. Upstream Docker Hub error status codes are proxied back." } }, "/api/roles": { "get": { "operationId": "get_api_roles", "tags": [ "roles" ], "summary": "List all roles (built-in and custom); available in setup mode or with an enterprise license", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": "string" }, "isSystem": { "type": "boolean" }, "permissions": { "type": "object", "properties": {} } }, "required": [ "id", "name", "isSystem" ] } }, "example": [ { "id": 0, "name": "string", "description": "string", "isSystem": true, "permissions": {} } ] } } }, "403": { "description": "Enterprise license required" }, "500": { "description": "Failed to read the roles" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_roles", "tags": [ "roles" ], "summary": "Create a custom role (enterprise; admin required when auth is enabled)", "parameters": [], "responses": { "201": { "description": "The created role" }, "400": { "description": "Name and permissions are required" }, "403": { "description": "Enterprise license required, or admin access required" }, "409": { "description": "A role with this name already exists" }, "500": { "description": "Failed to create the role" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentIds from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "permissions": { "type": "object", "properties": {} }, "environmentIds": { "type": "array", "items": { "type": "integer" } } }, "required": [ "name", "permissions" ] }, "example": { "name": "Operators", "description": "Can manage containers", "permissions": { "containers": [ "view", "edit" ] }, "environmentIds": [ 1, 2 ] } } } } } }, "/api/roles/{id}": { "get": { "operationId": "get_api_roles_id", "tags": [ "roles" ], "summary": "Get a single role by id; available in setup mode or with an enterprise license", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the role (from GET /api/roles)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": "string" }, "isSystem": { "type": "boolean" }, "permissions": { "type": "object", "properties": {} } }, "required": [ "id", "name", "isSystem" ] }, "example": { "id": 0, "name": "string", "description": "string", "isSystem": true, "permissions": {} } } } }, "400": { "description": "Role id is required" }, "403": { "description": "Enterprise license required" }, "404": { "description": "Role not found" }, "500": { "description": "Failed to read the role" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_roles_id", "tags": [ "roles" ], "summary": "Update a custom role (system roles cannot be modified; enterprise, admin required when auth is enabled)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the role (from GET /api/roles)" } ], "responses": { "200": { "description": "The updated role" }, "400": { "description": "Role id is required, or the role is a system role and cannot be modified" }, "403": { "description": "Enterprise license required, or admin access required" }, "404": { "description": "Role not found" }, "409": { "description": "A role with this name already exists" }, "500": { "description": "Failed to update the role" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentIds from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "permissions": { "type": "object", "properties": {} }, "environmentIds": { "type": "array", "items": { "type": "integer" } } } }, "example": { "description": "Updated description", "permissions": { "containers": [ "view" ] } } } } } }, "delete": { "operationId": "delete_api_roles_id", "tags": [ "roles" ], "summary": "Delete a custom role by id (system roles cannot be deleted; enterprise, admin required when auth is enabled)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the role (from GET /api/roles)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Role id is required, or the role is a system role and cannot be deleted" }, "403": { "description": "Enterprise license required, or admin access required" }, "404": { "description": "Role not found" }, "500": { "description": "Failed to delete the role" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules": { "get": { "operationId": "get_api_schedules", "tags": [ "schedules" ], "summary": "List all schedules (container/env auto-updates, git syncs, image-prune, backups, repo maintenance, system jobs)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "schedules": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "type": { "type": "string" }, "name": { "type": "string" }, "entityName": { "type": "string" }, "environmentId": { "type": "integer" }, "enabled": { "type": "boolean" }, "cronExpression": { "type": "string" }, "nextRun": { "type": "string" }, "isSystem": { "type": "boolean" } }, "required": [ "id", "type", "name", "entityName", "enabled", "isSystem" ] } } }, "required": [ "schedules" ] }, "example": { "schedules": [ { "id": 0, "type": "string", "name": "string", "entityName": "string", "environmentId": 0, "enabled": true, "cronExpression": "string", "nextRun": "string", "isSystem": true } ] } } } }, "500": { "description": "Unexpected error while assembling the schedule list" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules/{type}/{id}": { "delete": { "operationId": "delete_api_schedules_type_id", "tags": [ "schedules" ], "summary": "Delete (or disable, for policy-driven types) a schedule by type and id", "parameters": [ { "name": "type", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Schedule type (container_update, git_stack_sync, env_update_check, image_prune, backup, repo_prune, repo_check, repo_verify, system_cleanup)" }, { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Schedule id (semantics depend on type — container/git-stack/backup-config id, environment id, or a synthetic repo-policy id) (from GET /api/schedules)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid schedule id, invalid/unsupported type, or system_cleanup (cannot be removed)" }, "404": { "description": "Schedule (or backup destination, for repo_* types) not found" }, "500": { "description": "Unexpected error while deleting the schedule" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules/{type}/{id}/run": { "post": { "operationId": "post_api_schedules_type_id_run", "tags": [ "schedules" ], "summary": "Manually trigger a single run of a schedule (outside its cron), by type and id", "parameters": [ { "name": "type", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Schedule type (container_update, git_stack_sync, system_cleanup, env_update_check, image_prune, backup, repo_prune, repo_check, repo_verify)" }, { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Schedule id (semantics depend on type) (from GET /api/schedules)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" } }, "required": [ "success", "message" ] }, "example": { "success": true, "message": "string" } } } }, "400": { "description": "Invalid schedule id/type, or the triggered task itself reported failure" }, "404": { "description": "Schedule, backup config, or backup destination not found (or backup feature disabled)" }, "500": { "description": "Unexpected error while triggering the schedule" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules/{type}/{id}/toggle": { "post": { "operationId": "post_api_schedules_type_id_toggle", "tags": [ "schedules" ], "summary": "Toggle a schedule's enabled/disabled state (registers/unregisters the croner job accordingly)", "parameters": [ { "name": "type", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Schedule type (container_update, git_stack_sync, env_update_check, image_prune, backup, repo_prune, repo_check, repo_verify, system_cleanup)" }, { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Schedule id (semantics depend on type) (from GET /api/schedules)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "enabled": { "type": "boolean" } }, "required": [ "success", "enabled" ] }, "example": { "success": true, "enabled": true } } } }, "400": { "description": "Invalid schedule id, unsupported type, or system_cleanup (cannot be paused)" }, "404": { "description": "Schedule, backup config, or backup destination not found" }, "500": { "description": "Unexpected error while toggling the schedule" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules/executions": { "get": { "operationId": "get_api_schedules_executions", "tags": [ "schedules" ], "summary": "List schedule execution history, filterable and paginated", "parameters": [ { "name": "scheduleType", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by schedule type (container_update, git_stack_sync, ...)" }, { "name": "scheduleId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by the numeric id of the schedule (from GET /api/schedules)" }, { "name": "environmentId", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by environment id (\"null\" for global/system schedules) (from GET /api/environments)" }, { "name": "status", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by execution status (queued/running/success/warning/failed/skipped)" }, { "name": "statuses", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated list of statuses (alternative to status)" }, { "name": "triggeredBy", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by trigger (cron/webhook/manual)" }, { "name": "fromDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "ISO date lower bound" }, { "name": "toDate", "in": "query", "required": false, "schema": { "type": "string" }, "description": "ISO date upper bound" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Page size (default 50)" }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Page offset (default 0)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "executions": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "scheduleType": { "type": "string" }, "status": { "type": "string" }, "startedAt": { "type": "string" } }, "required": [ "id", "scheduleType", "status", "startedAt" ] } }, "total": { "type": "integer" }, "limit": { "type": "integer" }, "offset": { "type": "integer" } }, "required": [ "executions", "total", "limit", "offset" ] }, "example": { "executions": [ { "id": 0, "scheduleType": "string", "status": "string", "startedAt": "string" } ], "total": 0, "limit": 0, "offset": 0 } } } }, "500": { "description": "Unexpected error while loading execution history" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules/executions/{id}": { "get": { "operationId": "get_api_schedules_executions_id", "tags": [ "schedules" ], "summary": "Get a single schedule execution, including its logs", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Execution id (from GET /api/schedules/executions)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "scheduleType": { "type": "string" }, "status": { "type": "string" }, "startedAt": { "type": "string" }, "log": { "type": "string" } }, "required": [ "id", "scheduleType", "status", "startedAt" ] }, "example": { "id": 0, "scheduleType": "string", "status": "string", "startedAt": "string", "log": "string" } } } }, "400": { "description": "Invalid execution id" }, "404": { "description": "Execution not found" }, "500": { "description": "Unexpected error while loading the execution" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "operationId": "delete_api_schedules_executions_id", "tags": [ "schedules" ], "summary": "Delete a single schedule execution record", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Execution id (from GET /api/schedules/executions)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid execution id" }, "403": { "description": "Permission denied (RBAC 'schedules:edit' missing)" }, "500": { "description": "Unexpected error while deleting the execution" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules/settings": { "get": { "operationId": "get_api_schedules_settings", "tags": [ "schedules" ], "summary": "Get the schedules-page display preference (hide system jobs), per-user with a global fallback", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "hideSystemJobs": { "type": "boolean" } }, "required": [ "hideSystemJobs" ] }, "example": { "hideSystemJobs": true } } } }, "500": { "description": "Unexpected error while loading the setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_schedules_settings", "tags": [ "schedules" ], "summary": "Update the schedules-page display preference (hide system jobs) for the current user", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "hideSystemJobs": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true, "hideSystemJobs": true } } } }, "400": { "description": "hideSystemJobs is not a boolean" }, "500": { "description": "Unexpected error while saving the setting" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "hideSystemJobs": { "type": "boolean" } } }, "example": { "hideSystemJobs": true } } } } } }, "/api/schedules/stream": { "get": { "operationId": "get_api_schedules_stream", "tags": [ "schedules" ], "summary": "Server-Sent Events stream that polls and pushes schedule updates (same shape as GET /api/schedules)", "parameters": [], "responses": { "200": { "description": "text/event-stream response, periodically emitting the current schedule list as SSE \"data:\" messages" }, "403": { "description": "Permission denied (RBAC 'schedules:view' missing)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/schedules/system/{id}/toggle": { "post": { "operationId": "post_api_schedules_system_id_toggle", "tags": [ "schedules" ], "summary": "Toggle one of the three built-in system cleanup jobs (schedule/event/scanner cleanup) on or off", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "System schedule id (1=schedule cleanup, 2=event cleanup, 4=scanner cleanup) (from GET /api/schedules)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "enabled": { "type": "boolean" } }, "required": [ "success", "enabled" ] }, "example": { "success": true, "enabled": true } } } }, "400": { "description": "Invalid or unknown system schedule id" }, "403": { "description": "Permission denied (RBAC 'settings:edit' missing)" }, "500": { "description": "Unexpected error while toggling the job" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/secret-providers": { "get": { "operationId": "get_api_secret-providers", "tags": [ "secret-providers" ], "summary": "List configured secret providers (summaries never include the decrypted config)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "type": { "type": "string" } }, "required": [ "id", "name", "type" ] } }, "example": [ { "id": 0, "name": "string", "type": "string" } ] } } }, "403": { "description": "Permission denied (needs secrets:view)" }, "500": { "description": "Failed to fetch secret providers" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_secret-providers", "tags": [ "secret-providers" ], "summary": "Create a secret provider (Vault, Infisical, Doppler, 1Password Connect)", "parameters": [], "responses": { "201": { "description": "Response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "type": { "type": "string" } }, "required": [ "id", "name", "type" ] }, "example": { "id": 0, "name": "string", "type": "string" } } } }, "400": { "description": "Name and type are required, unknown provider type, config missing, or a provider with this name already exists" }, "403": { "description": "Permission denied (needs secrets:create)" }, "500": { "description": "Failed to create secret provider" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "type": "string" }, "config": { "type": "string" } }, "required": [ "name", "type", "config" ] }, "example": { "name": "string", "type": "string", "config": "string" } } } } } }, "/api/secret-providers/{id}": { "get": { "operationId": "get_api_secret-providers_id", "tags": [ "secret-providers" ], "summary": "Get one secret provider with its non-secret config coordinates (the token is redacted out)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The secret provider id" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "type": { "type": "string" }, "config": { "type": "string" } }, "required": [ "id", "name", "type", "config" ] }, "example": { "id": 0, "name": "string", "type": "string", "config": "string" } } } }, "400": { "description": "Invalid secret provider ID" }, "403": { "description": "Permission denied (needs secrets:view)" }, "404": { "description": "Secret provider not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_secret-providers_id", "tags": [ "secret-providers" ], "summary": "Update a secret provider (name, type, and/or rotate its config); omitted fields are left unchanged", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The secret provider id" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "type": { "type": "string" } }, "required": [ "id", "name", "type" ] }, "example": { "id": 0, "name": "string", "type": "string" } } } }, "400": { "description": "Invalid ID, empty name, unknown type, config not an object, or the name already exists" }, "403": { "description": "Permission denied (needs secrets:edit)" }, "404": { "description": "Secret provider not found" }, "500": { "description": "Failed to update secret provider" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "type": "string" }, "config": { "type": "string" } } }, "example": { "name": "string", "type": "string", "config": "string" } } } } }, "delete": { "operationId": "delete_api_secret-providers_id", "tags": [ "secret-providers" ], "summary": "Delete a secret provider", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The secret provider id" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid secret provider ID" }, "403": { "description": "Permission denied (needs secrets:delete)" }, "404": { "description": "Secret provider not found" }, "500": { "description": "Failed to delete secret provider" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/secret-providers/{id}/probe": { "post": { "operationId": "post_api_secret-providers_id_probe", "tags": [ "secret-providers" ], "summary": "Live-probe which key NAMES exist in a stored provider (names only, values never leave the server)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The secret provider id" } ], "responses": { "200": { "description": "ok=true returns the key names found via the bulk selector and the resolved op:// refs; ok=false folds a provider error in (still 200) so a slow or auth-gated provider never breaks the editor", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "bulkKeys": { "type": "array", "items": { "type": "string" } }, "resolvedRefs": { "type": "array", "items": { "type": "string" } }, "error": { "type": "string" } }, "required": [ "ok" ] }, "example": { "ok": true, "bulkKeys": [ "string" ], "resolvedRefs": [ "string" ], "error": "string" } } } }, "400": { "description": "Invalid ID or unknown provider type" }, "403": { "description": "Permission denied (needs secrets:view)" }, "404": { "description": "Secret provider not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "selector": { "type": "string" }, "refs": { "type": "array", "items": { "type": "string" } } } }, "example": { "selector": "prod", "refs": [ "op://vault/db/password" ] } } } } } }, "/api/secret-providers/{id}/test": { "post": { "operationId": "post_api_secret-providers_id_test", "tags": [ "secret-providers" ], "summary": "Test a stored provider's connectivity, optionally against typed override fields the edit form would save", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The secret provider id" } ], "responses": { "200": { "description": "ok=false carries the connection error (still 200 so the edit form can show it inline)", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "ok" ] }, "example": { "ok": true, "error": "string" } } } }, "400": { "description": "Invalid secret provider ID" }, "403": { "description": "Permission denied (needs secrets:view)" }, "404": { "description": "Secret provider not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "config": { "type": "string" } } }, "example": { "config": { "host": "https://vault.example.com", "mount": "secret" } } } } } } }, "/api/secret-providers/test": { "post": { "operationId": "post_api_secret-providers_test", "tags": [ "secret-providers" ], "summary": "Test an unsaved provider config (before creating the provider)", "parameters": [], "responses": { "200": { "description": "ok=false carries the reason (invalid type, missing config, or a connection error) - still 200 so the form shows it inline", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "ok" ] }, "example": { "ok": true, "error": "string" } } } }, "400": { "description": "Invalid request body" }, "403": { "description": "Permission denied (needs secrets:create)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "type": { "type": "string" }, "config": { "type": "string" } }, "required": [ "type", "config" ] }, "example": { "type": "vault", "config": { "host": "https://vault.example.com", "mount": "secret", "token": "hvs...." } } } } } } }, "/api/self-update": { "post": { "operationId": "post_api_self-update", "tags": [ "self-update" ], "summary": "Pull a new Dockhand image and hand off to an updater sidecar that replaces this running container", "parameters": [], "responses": { "200": { "description": "text/event-stream SSE response (steps: pulling_image, building_config, pulling_updater, creating_container, launching_updater, then a \"launched\"/\"error\" event) — or, with \"Accept: application/json\", the final event as plain JSON" }, "400": { "description": "newImage missing, not running in Docker, or the Docker socket is read-only" }, "403": { "description": "Admin access required" }, "500": { "description": "Failed to inspect own container or determine its name before starting the update" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "newImage": { "type": "string" } }, "required": [ "newImage" ] }, "example": { "newImage": "fnsys/dockhand:v1.0.40" } } } } } }, "/api/self-update/check": { "get": { "operationId": "get_api_self-update_check", "tags": [ "self-update" ], "summary": "Check whether a newer Dockhand image is available (version-tag compare via GitHub changelog, or registry-digest compare for mutable tags like :latest)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "updateAvailable": { "type": "boolean" }, "currentImage": { "type": "string" }, "newImage": { "type": "string" }, "latestVersion": { "type": "string" }, "currentDigest": { "type": "string" }, "newDigest": { "type": "string" }, "containerName": { "type": "string" }, "isComposeManaged": { "type": "boolean" }, "isLocalImage": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "updateAvailable" ] }, "example": { "updateAvailable": true, "currentImage": "string", "newImage": "string", "latestVersion": "string", "currentDigest": "string", "newDigest": "string", "containerName": "string", "isComposeManaged": true, "isLocalImage": true, "error": "string" } } } }, "403": { "description": "Admin access required" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/self-update/progress": { "get": { "operationId": "get_api_self-update_progress", "tags": [ "self-update" ], "summary": "Poll the self-update sidecar container's logs and exit state for progress tracking", "parameters": [ { "name": "id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Updater container id (from POST /api/self-update)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "logs": { "type": "string" }, "status": { "type": "string" }, "exitCode": { "type": "integer" } }, "required": [ "logs", "status" ] }, "example": { "logs": "string", "status": "string", "exitCode": 0 } } } }, "400": { "description": "Container ID is required" }, "403": { "description": "Admin access required" }, "500": { "description": "Failed to inspect the updater container, or failed to fetch its progress" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/settings/general": { "get": { "operationId": "get_api_settings_general", "tags": [ "settings" ], "summary": "Get global (instance-wide) general settings", "parameters": [], "responses": { "200": { "description": "Successful response" }, "401": { "description": "Not authenticated" }, "500": { "description": "Failed to load settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_settings_general", "tags": [ "settings" ], "summary": "Update global general settings (all fields optional; only supplied keys are written)", "parameters": [], "responses": { "200": { "description": "Successful response" }, "403": { "description": "Permission denied (needs settings:edit)" }, "500": { "description": "Failed to save settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "A large flat settings bag - theme/fonts, scanner defaults, cleanup schedules, event/metrics collection, editor options (e.g. editorIndentGuides), and more.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "animateIcons": { "type": "boolean" }, "editorIndentGuides": { "type": "boolean" }, "coloredActionButtons": { "type": "boolean" }, "lightTheme": { "type": "string" }, "darkTheme": { "type": "string" }, "defaultTimezone": { "type": "string" }, "logBufferSizeKb": { "type": "integer" }, "externalStackPaths": { "type": "string" } } }, "example": { "animateIcons": true, "editorIndentGuides": true, "coloredActionButtons": true, "lightTheme": "string", "darkTheme": "string", "defaultTimezone": "string", "logBufferSizeKb": 0, "externalStackPaths": "string" } } } } } }, "/api/settings/navigation": { "get": { "operationId": "get_api_settings_navigation", "tags": [ "settings" ], "summary": "Get the effective navigation preferences (landing page, env-click target) plus the raw global and per-user values", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "effective": { "type": "object", "properties": { "landingPage": { "type": "string" }, "envClickPage": { "type": "string" } }, "required": [ "landingPage", "envClickPage" ] }, "global": { "type": "object", "properties": { "landingPage": { "type": "string" }, "envClickPage": { "type": "string" } } }, "user": { "type": "object", "properties": { "landingPage": { "type": "string" }, "envClickPage": { "type": "string" } } } } }, "example": { "effective": { "landingPage": "dashboard", "envClickPage": "containers" }, "global": { "landingPage": null, "envClickPage": null }, "user": null } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_settings_navigation", "tags": [ "settings" ], "summary": "Update navigation preferences (landing page, env-click target) at global or per-user scope", "parameters": [ { "name": "scope", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Preference scope to update — \"global\" (default) or \"user\"" } ], "responses": { "200": { "description": "The updated preferences ({success:true, global:{...}} for scope=global, or {success:true, user:{...}} for scope=user)" }, "400": { "description": "Invalid input — invalid landingPage/envClickPage value, or scope=user requested while authentication is disabled" }, "401": { "description": "Not authenticated (scope=user, authentication is enabled but no valid session)" }, "403": { "description": "Permission denied (scope=global, requires settings:edit)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "landingPage": { "type": "string" }, "envClickPage": { "type": "string" } } }, "example": { "landingPage": "containers", "envClickPage": "logs" } } } } } }, "/api/settings/scanner": { "get": { "operationId": "get_api_settings_scanner", "tags": [ "settings" ], "summary": "Get the vulnerability-scanner settings for an environment, plus (unless settingsOnly) scanner availability, versions and optional update info", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id to read scanner settings for (falls back to global defaults) (from GET /api/environments)" }, { "name": "checkUpdates", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "When true, also check the scanner images for available updates (slower)" }, { "name": "settingsOnly", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "When true, return only settings + defaults and skip the Docker availability/version checks" } ], "responses": { "200": { "description": "Scanner settings and (unless settingsOnly) availability, versions, updates and defaults" }, "403": { "description": "Permission denied (missing settings:view for the environment)" }, "500": { "description": "Failed to read the scanner settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_settings_scanner", "tags": [ "settings" ], "summary": "Save the vulnerability-scanner settings for an environment", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "settings": { "type": "object", "properties": { "scanner": { "type": "string" }, "grypeArgs": { "type": "string" }, "trivyArgs": { "type": "string" } }, "required": [ "scanner", "grypeArgs", "trivyArgs" ] } }, "required": [ "success" ] }, "example": { "success": true, "settings": { "scanner": "string", "grypeArgs": "string", "trivyArgs": "string" } } } } }, "400": { "description": "Invalid scanner type (must be none, grype, trivy or both)" }, "403": { "description": "Permission denied (missing settings:edit for the environment)" }, "500": { "description": "Failed to save the scanner settings" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "scanner": { "type": "string" }, "grypeArgs": { "type": "string" }, "trivyArgs": { "type": "string" }, "envId": { "type": "integer" } } }, "example": { "scanner": "grype", "grypeArgs": "-o json -v {image}", "trivyArgs": "image --format json {image}", "envId": 1 } } } } }, "delete": { "operationId": "delete_api_settings_scanner", "tags": [ "settings" ], "summary": "Remove the scanner images (grype/trivy) and clean up scanner database volumes for an environment", "parameters": [ { "name": "removeImages", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Must be true to actually perform the removal (required)" }, { "name": "scanner", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Which scanner image to remove (grype or trivy); omit to remove both" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id whose scanner images should be removed (required) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "removed": { "type": "array", "items": { "type": "string" } }, "errors": { "type": "array", "items": { "type": "string" } } }, "required": [ "success" ] }, "example": { "success": true, "removed": [ "string" ], "errors": [ "string" ] } } } }, "400": { "description": "The removeImages parameter is required, or the environment id is missing" }, "403": { "description": "Permission denied (missing settings:edit for the environment)" }, "404": { "description": "Environment not found" }, "500": { "description": "Failed to remove the scanner images" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/settings/scanner/cache": { "delete": { "operationId": "delete_api_settings_scanner_cache", "tags": [ "settings" ], "summary": "Clear the vulnerability-scanner cache (local volumes/bind-mount dirs plus each remote environment's scanner volume)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "removedVolumes": { "type": "array", "items": { "type": "string" } }, "removedDirs": { "type": "array", "items": { "type": "string" } }, "skippedEnvironments": { "type": "array", "items": { "type": "string" } } }, "required": [ "success", "removedVolumes", "removedDirs", "skippedEnvironments" ] }, "example": { "success": true, "removedVolumes": [ "string" ], "removedDirs": [ "string" ], "skippedEnvironments": [ "string" ] } } } }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to clear scanner cache" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/settings/semver": { "get": { "operationId": "get_api_settings_semver", "tags": [ "settings" ], "summary": "Get the global newer-version-tag (semver) detection config", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "maxBump": { "type": "string" }, "matchFlavor": { "type": "boolean" }, "includePrerelease": { "type": "boolean" } }, "required": [ "enabled", "maxBump", "matchFlavor", "includePrerelease" ] }, "example": { "enabled": true, "maxBump": "minor", "matchFlavor": true, "includePrerelease": false } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_settings_semver", "tags": [ "settings" ], "summary": "Set the global newer-version-tag (semver) detection config", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (needs settings:edit)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "maxBump": { "type": "string" }, "matchFlavor": { "type": "boolean" }, "includePrerelease": { "type": "boolean" } } }, "example": { "enabled": true, "maxBump": "minor", "matchFlavor": true, "includePrerelease": false } } } } } }, "/api/settings/theme": { "get": { "operationId": "get_api_settings_theme", "tags": [ "settings" ], "summary": "Public theme + editor display settings (used by the login page before auth)", "parameters": [], "responses": { "200": { "description": "Light/dark theme, fonts, and editor display flags (e.g. editorIndentGuides)" } }, "security": [] } }, "/api/stacks": { "get": { "operationId": "get_api_stacks", "tags": [ "stacks" ], "summary": "List compose stacks for one environment (internal, external, and git)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id" } ], "responses": { "200": { "description": "Successful response" }, "403": { "description": "Permission denied (needs stacks:view)" }, "404": { "description": "Environment not found" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_stacks", "tags": [ "stacks" ], "summary": "Create and (optionally) deploy a compose stack", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Target environment id" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Invalid request (e.g. missing name/compose, or secretProviderId wrong type)" }, "403": { "description": "Permission denied (needs stacks:create; binding a secret provider also needs secrets:view)" }, "500": { "description": "Failed to create or deploy the stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Writes the compose + .env to the stack dir, stores secrets in the DB, and with start deploys it. Can bind a secret provider.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "compose": { "type": "string" }, "composePath": { "type": "string" }, "envPath": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "string" } }, "rawEnvContent": { "type": "string" }, "secretProviderId": { "type": "integer" }, "start": { "type": "boolean" } }, "required": [ "name", "compose" ] }, "example": { "name": "string", "compose": "string", "composePath": "string", "envPath": "string", "envVars": [ "string" ], "rawEnvContent": "string", "secretProviderId": 0, "start": true } } } } } }, "/api/stacks/{name}": { "delete": { "operationId": "delete_api_stacks_name", "tags": [ "stacks" ], "summary": "Remove a stack completely (compose down + delete files + database cleanup)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" }, { "name": "force", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Force removal even if the compose down step fails" }, { "name": "volumes", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Also remove named volumes (docker compose down --volumes)" }, { "name": "files", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Delete the stack's on-disk files/directory too (default true; pass files=false to keep them on disk)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Compose down failed and force was not set" }, "403": { "description": "Permission denied, or access denied to this environment" }, "404": { "description": "Compose file not found for this stack" }, "500": { "description": "Unexpected error while removing the stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/{name}/check-path-change": { "post": { "operationId": "post_api_stacks_name_check-path-change", "tags": [ "stacks" ], "summary": "Check whether a proposed compose path moves the stack to a different directory and how many files the old directory still holds", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "hasChanges": { "type": "boolean" }, "oldDir": { "type": "string" }, "newDir": { "type": "string" }, "fileCount": { "type": "integer" }, "currentComposePath": { "type": "string" } }, "required": [ "hasChanges", "fileCount" ] }, "example": { "hasChanges": true, "oldDir": "/opt/stacks/old", "newDir": "/opt/stacks/web", "fileCount": 3, "currentComposePath": "/opt/stacks/old/compose.yaml" } } } }, "403": { "description": "Permission denied (requires stacks:edit)" }, "500": { "description": "Failed to check path changes" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "newComposePath": { "type": "string" } }, "required": [ "newComposePath" ] }, "example": { "newComposePath": "/opt/stacks/web/compose.yaml" } } } } } }, "/api/stacks/{name}/compose": { "get": { "operationId": "get_api_stacks_name_compose", "tags": [ "stacks" ], "summary": "Get a stack's compose file content plus its resolved compose/env paths", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the stack belongs to" } ], "responses": { "200": { "description": "Successful response" }, "403": { "description": "Permission denied (needs stacks:view)" }, "404": { "description": "Stack or compose file not found" }, "500": { "description": "Failed to read the compose file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_stacks_name_compose", "tags": [ "stacks" ], "summary": "Save a stack's compose file (and optionally relocate it, bind a secret provider, and redeploy)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the stack belongs to" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Invalid request (e.g. missing content, or secretProviderId wrong type)" }, "403": { "description": "Permission denied (needs stacks:edit; binding a secret provider also needs secrets:view)" }, "500": { "description": "Failed to save or deploy the compose file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Every accepted PUT persists the compose content; with restart it also redeploys. Supports moving the compose/env to a new path and binding a secret provider.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" }, "composePath": { "type": "string" }, "envPath": { "type": "string" }, "oldComposePath": { "type": "string" }, "oldEnvPath": { "type": "string" }, "moveFromDir": { "type": "string" }, "restart": { "type": "boolean" }, "secretProviderId": { "type": "integer" } }, "required": [ "content" ] }, "example": { "content": "string", "composePath": "string", "envPath": "string", "oldComposePath": "string", "oldEnvPath": "string", "moveFromDir": "string", "restart": true, "secretProviderId": 0 } } } } } }, "/api/stacks/{name}/delete-preview": { "get": { "operationId": "get_api_stacks_name_delete-preview", "tags": [ "stacks" ], "summary": "Preview the on-disk directories and named volumes a delete-with-files would remove for a stack", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "stackName": { "type": "string" }, "sourceType": { "type": "string" }, "stackDir": { "type": "string" }, "gitDir": { "type": "string" }, "namedVolumes": { "type": "array", "items": { "type": "string" } }, "canDeleteFiles": { "type": "boolean" } }, "required": [ "stackName", "namedVolumes", "canDeleteFiles" ] }, "example": { "stackName": "web", "sourceType": "internal", "stackDir": "/opt/stacks/web", "gitDir": null, "namedVolumes": [ "web_data" ], "canDeleteFiles": true } } } }, "403": { "description": "Permission denied (requires stacks:remove)" }, "500": { "description": "Failed to compute the deletion preview" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/{name}/deploy": { "post": { "operationId": "post_api_stacks_name_deploy", "tags": [ "stacks" ], "summary": "Deploy (docker compose up) a stack, optionally pulling images, building, and force-recreating; progress and the final result stream over Server-Sent Events", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Server-Sent-Events job stream with progress events and a final result event ({success, output})" }, "403": { "description": "Permission denied (requires stacks:start, or environment access denied on enterprise)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "pull": { "type": "boolean" }, "build": { "type": "boolean" }, "forceRecreate": { "type": "boolean" } } }, "example": { "pull": true, "build": false, "forceRecreate": false } } } } } }, "/api/stacks/{name}/down": { "post": { "operationId": "post_api_stacks_name_down", "tags": [ "stacks" ], "summary": "Take a stack down (docker compose down — removes containers, keeps files), asynchronously", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Fire-and-forget job id — poll GET /api/jobs/{jobId} for the result. Send \"Accept: application/json\" (without text/event-stream) to instead block and receive the final {success,output|error} synchronously.", "content": { "application/json": { "schema": { "type": "object", "properties": { "jobId": { "type": "string" } }, "required": [ "jobId" ] }, "example": { "jobId": "3f9c5b1a-2e4d-4a6f-9b0a-1c7d8e9f0a1b" } } } }, "403": { "description": "Permission denied, or access denied to this environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "removeVolumes": { "type": "boolean" } } }, "example": { "removeVolumes": false } } } } } }, "/api/stacks/{name}/env": { "get": { "operationId": "get_api_stacks_name_env", "tags": [ "stacks" ], "summary": "Get a stack's env vars plus injected provider keys", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the stack belongs to" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "variables": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" }, "isSecret": { "type": "boolean" } }, "required": [ "key", "value", "isSecret" ] } }, "injectedSecretKeys": { "type": "array", "items": { "type": "string" } }, "secretProvider": { "type": "string" } }, "required": [ "variables" ] }, "example": { "variables": [ { "key": "string", "value": "string", "isSecret": true } ], "injectedSecretKeys": [ "string" ], "secretProvider": "string" } } } }, "403": { "description": "Permission denied (needs stacks:view)" }, "500": { "description": "Failed to read env vars" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "The source of `variables` depends on the stack type. For a GIT stack, ALL variables (secret and non-secret) come from the database via this endpoint - the DB is the canonical store the UI, `POST /env/validate`, and deploys read. For an INTERNAL/adopted stack, non-secrets come from the on-disk `.env` file (written via `PUT /env/raw`) and only secrets come from the DB. Secret values are masked as `***` and never returned in plaintext. Each variable is `{ key, value, isSecret }`." }, "put": { "operationId": "put_api_stacks_name_env", "tags": [ "stacks" ], "summary": "Save a stack's env vars to the DB", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the stack belongs to" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Invalid variables payload" }, "403": { "description": "Permission denied (needs stacks:edit)" }, "500": { "description": "Failed to save env vars" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Persists EVERY variable in the body to the database (secrets and non-secrets alike). For a GIT stack this is the canonical store - non-secret overrides and secrets both belong here, NOT in the `.env` file. For an INTERNAL/adopted stack the DB is used for secrets while non-secrets are expected in the `.env` file (written via `PUT /env/raw`); posting a non-secret here still saves it but the file remains the source the UI reads for that type. A masked secret value of `***` is left unchanged (the stored value is preserved). Each item is `{ key, value, isSecret? }`; a request whose `variables` are not such objects is rejected with 400.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "variables": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" }, "isSecret": { "type": "boolean" } }, "required": [ "key", "value" ] } } }, "required": [ "variables" ] }, "example": { "variables": [ { "key": "DB_HOST", "value": "db", "isSecret": false }, { "key": "DB_PASSWORD", "value": "s3cret", "isSecret": true } ] } } } } } }, "/api/stacks/{name}/env/raw": { "get": { "operationId": "get_api_stacks_name_env_raw", "tags": [ "stacks" ], "summary": "Get the raw .env file content as-is (comments and formatting preserved) for a stack", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" }, "noEnvFile": { "type": "boolean" } }, "required": [ "content" ] }, "example": { "content": "FOO=bar\n# comment\nBAZ=qux\n" } } } }, "403": { "description": "Permission denied (requires stacks:view, or environment access denied on enterprise)" }, "500": { "description": "Failed to get environment file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_stacks_name_env_raw", "tags": [ "stacks" ], "summary": "Write raw .env file content to disk for a stack; empty content deletes the .env file, and masked \"***\" placeholders are rejected to avoid corrupting secrets", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "noEnvFile": { "type": "boolean" }, "deleted": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "Invalid body (content string required) or refusal to write a masked \"***\" placeholder" }, "403": { "description": "Permission denied (requires stacks:edit, or environment access denied on enterprise)" }, "500": { "description": "Failed to save environment file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" } }, "required": [ "content" ] }, "example": { "content": "FOO=bar\nBAZ=qux\n" } } } } } }, "/api/stacks/{name}/env/validate": { "post": { "operationId": "post_api_stacks_name_env_validate", "tags": [ "stacks" ], "summary": "Validate a stack's defined environment variables against the variables required/optional by its compose file (compose content and the defined variable list may be supplied in the body, otherwise loaded from the saved file/DB)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "valid": { "type": "boolean" }, "required": { "type": "array", "items": { "type": "string" } }, "optional": { "type": "array", "items": { "type": "string" } }, "defined": { "type": "array", "items": { "type": "string" } }, "missing": { "type": "array", "items": { "type": "string" } }, "unused": { "type": "array", "items": { "type": "string" } } }, "required": [ "valid", "required", "optional", "defined", "missing", "unused" ] }, "example": { "valid": false, "required": [ "IMAGE" ], "optional": [], "defined": [], "missing": [ "IMAGE" ], "unused": [] } } } }, "400": { "description": "No compose content provided and no saved compose file found" }, "403": { "description": "Permission denied (requires stacks:view, or environment access denied on enterprise)" }, "500": { "description": "Failed to validate environment variables" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "compose": { "type": "string" }, "variables": { "type": "array", "items": { "type": "string" } } } }, "example": { "compose": "services:\n web:\n image: ${IMAGE}", "variables": [ "IMAGE" ] } } } } } }, "/api/stacks/{name}/icon": { "get": { "operationId": "get_api_stacks_name_icon", "tags": [ "stacks" ], "summary": "Get a stack's uploaded custom icon (raw image/webp bytes, not JSON)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the stack belongs to" } ], "responses": { "200": { "description": "Binary image/webp response body, Cache-Control public max-age=3600" }, "403": { "description": "Permission denied (needs stacks:view)" }, "404": { "description": "No custom icon set for this stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_stacks_name_icon", "tags": [ "stacks" ], "summary": "Set a stack's icon - either a name reference or an uploaded image", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the stack belongs to" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "icon": { "type": "string" } }, "required": [ "success", "icon" ] }, "example": { "success": true, "icon": "selfhst:plex" } } } }, "400": { "description": "Neither icon nor image supplied, or image exceeds the size limit" }, "403": { "description": "Permission denied (needs stacks:edit)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Body carries EITHER `icon` (a lucide name or `selfhst:`) to set a referenced icon, OR `image` (a base64-encoded data URL, ~300KB limit) to upload a custom one (stored as `custom:stack`). One of the two is required.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "icon": { "type": "string" }, "image": { "type": "string" } } }, "example": { "icon": "string", "image": "string" } } } } }, "delete": { "operationId": "delete_api_stacks_name_icon", "tags": [ "stacks" ], "summary": "Remove a stack's custom icon", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id the stack belongs to" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (needs stacks:edit)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/{name}/relocate": { "post": { "operationId": "post_api_stacks_name_relocate", "tags": [ "stacks" ], "summary": "Move all stack files from the old directory to a new location, update the stored compose/env paths, and return the refreshed compose/env content", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "movedFiles": { "type": "array", "items": { "type": "string" } }, "errors": { "type": "array", "items": { "type": "string" } }, "composeContent": { "type": "string" }, "rawEnvContent": { "type": "string" }, "envVars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "value": { "type": "string" }, "isSecret": { "type": "boolean" } }, "required": [ "key", "value", "isSecret" ] } } }, "required": [ "success", "movedFiles", "composeContent", "rawEnvContent", "envVars" ] }, "example": { "success": true, "movedFiles": [ "compose.yaml", ".env" ], "composeContent": "services: {}", "rawEnvContent": "FOO=bar\n", "envVars": [ { "key": "FOO", "value": "bar", "isSecret": false } ] } } } }, "400": { "description": "oldDir and newComposePath are required, or the source directory does not exist" }, "403": { "description": "Permission denied (requires stacks:edit)" }, "500": { "description": "Failed to relocate stack" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "oldDir": { "type": "string" }, "newComposePath": { "type": "string" }, "newEnvPath": { "type": "string" } }, "required": [ "oldDir", "newComposePath" ] }, "example": { "oldDir": "/opt/stacks/old", "newComposePath": "/opt/stacks/web/compose.yaml", "newEnvPath": "/opt/stacks/web/.env" } } } } } }, "/api/stacks/{name}/restart": { "post": { "operationId": "post_api_stacks_name_restart", "tags": [ "stacks" ], "summary": "Restart a stack (mode=restart|ordered|recreate); progress and the final result stream over Server-Sent Events", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" }, { "name": "mode", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Restart mode — \"recreate\" recreates containers (new IDs, re-pull), \"ordered\" does a stop+start honoring depends_on ordering (same IDs), anything else is a plain in-place restart" } ], "responses": { "200": { "description": "Server-Sent-Events job stream with a final result event ({success, output})" }, "403": { "description": "Permission denied (requires stacks:restart, or environment access denied on enterprise)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/{name}/start": { "post": { "operationId": "post_api_stacks_name_start", "tags": [ "stacks" ], "summary": "Start a stack (docker compose start/up), asynchronously", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Fire-and-forget job id — poll GET /api/jobs/{jobId} for the result. Send \"Accept: application/json\" (without text/event-stream) to instead block and receive the final {success,output|error} synchronously.", "content": { "application/json": { "schema": { "type": "object", "properties": { "jobId": { "type": "string" } }, "required": [ "jobId" ] }, "example": { "jobId": "3f9c5b1a-2e4d-4a6f-9b0a-1c7d8e9f0a1b" } } } }, "403": { "description": "Permission denied, or access denied to this environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/{name}/stop": { "post": { "operationId": "post_api_stacks_name_stop", "tags": [ "stacks" ], "summary": "Stop a stack (docker compose stop), asynchronously", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment id (from GET /api/environments)" } ], "responses": { "200": { "description": "Fire-and-forget job id — poll GET /api/jobs/{jobId} for the result. Send \"Accept: application/json\" (without text/event-stream) to instead block and receive the final {success,output|error} synchronously.", "content": { "application/json": { "schema": { "type": "object", "properties": { "jobId": { "type": "string" } }, "required": [ "jobId" ] }, "example": { "jobId": "3f9c5b1a-2e4d-4a6f-9b0a-1c7d8e9f0a1b" } } } }, "403": { "description": "Permission denied, or access denied to this environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/{name}/validate": { "post": { "operationId": "post_api_stacks_name_validate", "tags": [ "stacks" ], "summary": "Run the Compose Validate preflight linter on a compose file and return findings with severities and line numbers (requires the 'view' permission)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Stack name (used to exclude the stack's own containers from cross-stack collision checks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID for the context-aware checks (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "findings": { "type": "array", "items": { "type": "object", "properties": { "ruleId": { "type": "string" }, "severity": { "type": "string" }, "message": { "type": "string" }, "hint": { "type": "string" }, "service": { "type": "string" }, "line": { "type": "integer" }, "fix": { "type": "string" }, "fixDescription": { "type": "string" } }, "required": [ "ruleId", "severity", "message" ] } }, "counts": { "type": "object", "properties": { "error": { "type": "integer" }, "warn": { "type": "integer" }, "info": { "type": "integer" } }, "required": [ "error", "warn", "info" ] } }, "required": [ "findings", "counts" ] }, "example": { "findings": [ { "ruleId": "LATEST_TAG", "severity": "warn", "message": "\"web\" uses `:latest` (nginx:latest)", "hint": "Pin a version tag for reproducible deploys and to enable newer-version detection.", "service": "web", "line": 3 } ], "counts": { "error": 0, "warn": 1, "info": 0 } } } } }, "400": { "description": "compose content is required" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "compose": { "type": "string" }, "config": { "type": "object", "properties": { "disabled": { "type": "array", "items": { "type": "string" } }, "severity": { "type": "string" } } }, "envVars": { "type": "string" } }, "required": [ "compose" ] }, "example": { "compose": "services:\n web:\n image: nginx:latest\n ports: [\"8080:80\"]\n" } } } } } }, "/api/stacks/adopt": { "post": { "operationId": "post_api_stacks_adopt", "tags": [ "stacks" ], "summary": "Adopt previously discovered compose stacks into Dockhand for a given environment", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "adopted": { "type": "array", "items": { "type": "string" } }, "failed": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "error": { "type": "string" } }, "required": [ "name", "error" ] } } }, "required": [ "adopted", "failed" ] }, "example": { "adopted": [ "web" ], "failed": [] } } } }, "400": { "description": "No stacks provided, missing environmentId, or a stack is missing name/composePath" }, "403": { "description": "Permission denied (requires stacks:create)" }, "500": { "description": "Unexpected error while adopting stacks" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "stacks": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "composePath": { "type": "string" } }, "required": [ "name", "composePath" ] } }, "environmentId": { "type": "integer" } }, "required": [ "stacks", "environmentId" ] }, "example": { "stacks": [ { "name": "web", "composePath": "/opt/stacks/web/compose.yaml" } ], "environmentId": 1 } } } } } }, "/api/stacks/base-path": { "get": { "operationId": "get_api_stacks_base-path", "tags": [ "stacks" ], "summary": "Return the default Dockhand stacks directory ($DATA_DIR/stacks/) where new stacks are stored by default", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID used to select the local or environment-scoped stacks root" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "basePath": { "type": "string" } }, "required": [ "basePath" ] }, "example": { "basePath": "/data/stacks" } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/default-path": { "get": { "operationId": "get_api_stacks_default-path", "tags": [ "stacks" ], "summary": "Compute the default compose/env file paths for a new stack, either under a custom base location or under Dockhand's default stacks directory", "parameters": [ { "name": "name", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Stack name" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID (scopes the path under the environment name) (from GET /api/environments)" }, { "name": "location", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Custom base location path" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "stackDir": { "type": "string" }, "composePath": { "type": "string" }, "envPath": { "type": "string" }, "source": { "type": "string" } }, "required": [ "stackDir", "composePath", "envPath", "source" ] }, "example": { "stackDir": "/data/stacks/prod/web", "composePath": "/data/stacks/prod/web/compose.yaml", "envPath": "/data/stacks/prod/web/.env", "source": "default" } } } }, "400": { "description": "Stack name is required" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/path-hints": { "get": { "operationId": "get_api_stacks_path-hints", "tags": [ "stacks" ], "summary": "Return path hints (working directory and config file paths) extracted from a stack's Docker container labels", "parameters": [ { "name": "name", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Stack name (from GET /api/stacks)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the stack belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "stackName": { "type": "string" }, "workingDir": { "type": "string" }, "configFiles": { "type": "array", "items": { "type": "string" } } }, "required": [ "stackName" ] }, "example": { "stackName": "web", "workingDir": "/opt/stacks/web", "configFiles": [ "/opt/stacks/web/compose.yaml" ] } } } }, "400": { "description": "Stack name is required" }, "401": { "description": "Unauthorized" }, "500": { "description": "Failed to get path hints" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/scan": { "post": { "operationId": "post_api_stacks_scan", "tags": [ "stacks" ], "summary": "Scan a given filesystem path (or all configured external paths when none is given) for compose stacks, flagging which discovered stacks are already running", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "discovered": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" } }, "required": [ "name" ] } }, "adopted": { "type": "array", "items": { "type": "string" } }, "skipped": { "type": "array", "items": { "type": "string" } }, "errors": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "error": { "type": "string" } }, "required": [ "path", "error" ] } } }, "required": [ "discovered", "adopted", "skipped", "errors" ] }, "example": { "discovered": [ { "name": "web" } ], "adopted": [], "skipped": [], "errors": [] } } } }, "403": { "description": "Permission denied (requires stacks:create)" }, "500": { "description": "Unexpected error while scanning" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" } } }, "example": { "path": "/opt/stacks" } } } } } }, "/api/stacks/sources": { "get": { "operationId": "get_api_stacks_sources", "tags": [ "stacks" ], "summary": "List stack source records (their stored compose/env paths and source type, plus an env-var count)", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter to a single environment id" } ], "responses": { "200": { "description": "Successful response" }, "403": { "description": "Permission denied (needs stacks:view)" }, "500": { "description": "Failed to list stack sources" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/stacks/validate-path": { "post": { "operationId": "post_api_stacks_validate-path", "tags": [ "stacks" ], "summary": "Validate a candidate external stack path (exists, is a directory, no overlap with already-configured paths); validation failures are returned as 200 with valid=false", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "valid": { "type": "boolean" }, "error": { "type": "string" } }, "required": [ "valid" ] }, "example": { "valid": false, "error": "Path does not exist" } } } }, "403": { "description": "Permission denied (requires settings:edit)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" } }, "required": [ "path" ] }, "example": { "path": "/opt/external-stacks" } } } } } }, "/api/system": { "get": { "operationId": "get_api_system", "tags": [ "system" ], "summary": "Return aggregated system info — Docker daemon, host, Node.js runtime, database, and object counts for an environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment to collect Docker info for (Docker fields are null when omitted or unreachable) (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "docker": { "type": "string" }, "host": { "type": "string" }, "runtime": { "type": "string" }, "database": { "type": "string" }, "stats": { "type": "string" } }, "required": [ "runtime", "database", "stats" ] }, "example": { "docker": { "version": "27.0.3", "apiVersion": "1.46" }, "host": { "name": "docker-host", "cpus": 8, "memory": 16777216000 }, "runtime": { "runtimeName": "Node.js", "platform": "linux" }, "database": { "type": "SQLite", "schemaVersion": 42 }, "stats": { "containers": { "total": 10, "running": 8, "stopped": 2 }, "images": 25, "volumes": 5, "networks": 4, "stacks": 3 } } } } }, "403": { "description": "Permission denied, or (enterprise) no access to this environment" }, "500": { "description": "Failed to fetch system info" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/system/disk": { "get": { "operationId": "get_api_system_disk", "tags": [ "system" ], "summary": "Return Docker disk usage (df) for an environment, or null when collection is disabled, times out, or fails", "parameters": [ { "name": "env", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "ID of the environment to query disk usage for (from GET /api/environments)" } ], "responses": { "200": { "description": "diskUsage is null when SKIP_DF_COLLECTION is set, or the query times out or errors", "content": { "application/json": { "schema": { "type": "object", "properties": { "diskUsage": { "type": "string" } } }, "example": { "diskUsage": { "LayersSize": 1420000000, "Images": [], "Containers": [], "Volumes": [] } } } } }, "400": { "description": "Environment ID is required" }, "403": { "description": "Permission denied, or (enterprise) no access to this environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/system/files": { "get": { "operationId": "get_api_system_files", "tags": [ "system" ], "summary": "List the entries of a directory on Dockhand's local filesystem (protected paths are hidden)", "parameters": [ { "name": "path", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Absolute directory path to list (defaults to \"/\")" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" }, "parent": { "type": "string" }, "entries": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "path": { "type": "string" }, "type": { "type": "string" }, "size": { "type": "integer" }, "mtime": { "type": "string" }, "mode": { "type": "string" } }, "required": [ "name", "path", "type", "size", "mtime", "mode" ] } } }, "required": [ "path", "entries" ] }, "example": { "path": "/docker", "parent": "/", "entries": [ { "name": "stacks", "path": "/docker/stacks", "type": "directory", "size": 4096, "mtime": "2026-07-01T10:00:00.000Z", "mode": "755" } ] } } } }, "400": { "description": "The path exists but is not a directory" }, "403": { "description": "Permission denied, or the path is protected" }, "404": { "description": "Path not found" }, "500": { "description": "Failed to list directory" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_system_files", "tags": [ "system" ], "summary": "Create a directory on Dockhand's local filesystem (absolute path, no traversal, non-protected)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "path": { "type": "string" } }, "required": [ "success", "path" ] }, "example": { "success": true, "path": "/docker/stacks/myapp" } } } }, "400": { "description": "Path is missing, not absolute, or contains \"..\"" }, "403": { "description": "Permission denied, or the path is protected" }, "409": { "description": "Path already exists" }, "500": { "description": "Failed to create directory" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" } }, "required": [ "path" ] }, "example": { "path": "/docker/stacks/myapp" } } } } } }, "/api/system/files/content": { "get": { "operationId": "get_api_system_files_content", "tags": [ "system" ], "summary": "Read a text file from Dockhand's local filesystem (max 10MB, protected paths denied)", "parameters": [ { "name": "path", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Absolute path of the file to read" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" }, "content": { "type": "string" }, "size": { "type": "integer" }, "mtime": { "type": "string" } }, "required": [ "path", "content", "size", "mtime" ] }, "example": { "path": "/docker/stacks/myapp/compose.yaml", "content": "services:\n app:\n image: nginx", "size": 42, "mtime": "2026-07-01T10:00:00.000Z" } } } }, "400": { "description": "Path is missing, points to a directory, or the file exceeds 10MB" }, "403": { "description": "Permission denied, or the path is protected" }, "404": { "description": "File not found" }, "500": { "description": "Failed to read file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/templates": { "get": { "operationId": "get_api_templates", "tags": [ "templates" ], "summary": "Return the normalized app templates aggregated from all enabled template sources (server-side cached for 1 hour)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "type": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "logo": { "type": "string" }, "categories": { "type": "array", "items": { "type": "string" } }, "source": { "type": "string" }, "image": { "type": "string" }, "projectUrl": { "type": "string" }, "detailsUrl": { "type": "string" } }, "required": [ "id", "type", "title", "description", "logo", "categories", "source" ] } }, "example": [ { "id": "a1b2c3", "type": "container", "title": "Nginx", "description": "Web server", "logo": "https://example.com/nginx.png", "categories": [ "web" ], "source": "LinuxServer.io", "image": "lscr.io/linuxserver/nginx:latest", "projectUrl": "https://github.com/linuxserver/docker-nginx" } ] } } }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/templates/compose": { "post": { "operationId": "post_api_templates_compose", "tags": [ "templates" ], "summary": "Return the compose YAML for a template — generated for container templates, fetched from the repository for stack templates", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "compose": { "type": "string" } }, "required": [ "compose" ] }, "example": { "compose": "services:\n nginx:\n image: nginx:latest\n restart: unless-stopped\n" } } } }, "400": { "description": "Template is missing from the request body" }, "403": { "description": "Permission denied" }, "500": { "description": "Failed to generate or fetch the compose file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "template": { "type": "object", "properties": { "id": { "type": "string" }, "type": { "type": "string" }, "title": { "type": "string" }, "image": { "type": "string" }, "repository": { "type": "object", "properties": { "url": { "type": "string" }, "stackfile": { "type": "string" } } } } } }, "required": [ "template" ] }, "example": { "template": { "id": "a1b2c3", "type": "container", "title": "Nginx", "image": "nginx:latest" } } } } } } }, "/api/templates/sources": { "get": { "operationId": "get_api_templates_sources", "tags": [ "templates" ], "summary": "List all configured template sources (built-in and custom)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "enabled": { "type": "boolean" } }, "required": [ "id", "name", "url", "enabled" ] } }, "example": [ { "id": 1, "name": "LinuxServer.io", "url": "https://fleet.linuxserver.io/api/v1/images", "enabled": true } ] } } }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_templates_sources", "tags": [ "templates" ], "summary": "Add a custom template source with a name and URL", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "url": { "type": "string" }, "enabled": { "type": "boolean" } }, "required": [ "id", "name", "url", "enabled" ] }, "example": { "id": 4, "name": "My Templates", "url": "https://example.com/templates.json", "enabled": true } } } }, "400": { "description": "Name and URL are required" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string" } }, "required": [ "name", "url" ] }, "example": { "name": "My Templates", "url": "https://example.com/templates.json" } } } } }, "put": { "operationId": "put_api_templates_sources", "tags": [ "templates" ], "summary": "Update a template source — toggle enabled, or change its name/URL (only provided fields are changed)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } }, "required": [ "ok" ] }, "example": { "ok": true } } } }, "400": { "description": "Missing id" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "enabled": { "type": "boolean" }, "name": { "type": "string" }, "url": { "type": "string" } }, "required": [ "id" ] }, "example": { "id": 3, "enabled": false } } } } }, "delete": { "operationId": "delete_api_templates_sources", "tags": [ "templates" ], "summary": "Delete a custom template source by ID", "parameters": [ { "name": "id", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "ID of the template source to delete (from GET /api/templates/sources)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } }, "required": [ "ok" ] }, "example": { "ok": true } } } }, "400": { "description": "Missing id" }, "403": { "description": "Permission denied" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/users": { "get": { "operationId": "get_api_users", "tags": [ "users" ], "summary": "List all local/SSO users (any authenticated user may view — only mutations are RBAC-gated)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "isAdmin": { "type": "boolean" }, "isActive": { "type": "boolean" }, "isSso": { "type": "boolean" }, "authProvider": { "type": "string" } }, "required": [ "id", "username", "isAdmin", "isActive", "isSso", "authProvider" ] } }, "example": [ { "id": 1, "username": "admin", "email": "admin@example.com", "displayName": "Admin", "isAdmin": true, "isActive": true, "isSso": false, "authProvider": "local" } ] } } }, "401": { "description": "Not authenticated" }, "500": { "description": "Unexpected error while loading users" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_users", "tags": [ "users" ], "summary": "Create a local user (allowed without auth only during initial setup, before any admin exists)", "parameters": [], "responses": { "201": { "description": "The first user ever created (or every user in Free edition) automatically gets the Admin role", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "isAdmin": { "type": "boolean" } }, "required": [ "id", "username", "isAdmin" ] }, "example": { "id": 1, "username": "jdoe", "email": "jdoe@example.com", "isAdmin": true } } } }, "400": { "description": "Missing username/password, or password shorter than 8 characters" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated; the first admin during initial setup is exempt)" }, "403": { "description": "Permission denied (RBAC 'users:create' missing — only applies once an admin already exists)" }, "409": { "description": "Username already exists" }, "500": { "description": "Unexpected error while creating the user" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "username": { "type": "string" }, "email": { "type": "string" }, "password": { "type": "string" }, "displayName": { "type": "string" } }, "required": [ "username", "password" ] }, "example": { "username": "jdoe", "email": "jdoe@example.com", "password": "correct horse battery staple", "displayName": "Jane Doe" } } } } } }, "/api/users/{id}": { "get": { "operationId": "get_api_users_id", "tags": [ "users" ], "summary": "Get a single user by id (password hash is never returned; isAdmin is derived from role assignment)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the user (from GET /api/users)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "mfaEnabled": { "type": "boolean" }, "isAdmin": { "type": "boolean" }, "isActive": { "type": "boolean" }, "lastLogin": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "username", "mfaEnabled", "isAdmin", "isActive", "createdAt", "updatedAt" ] }, "example": { "id": 0, "username": "string", "email": "string", "displayName": "string", "mfaEnabled": true, "isAdmin": true, "isActive": true, "lastLogin": "string", "createdAt": "string", "updatedAt": "string" } } } }, "400": { "description": "User id is required" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "404": { "description": "User not found" }, "500": { "description": "Failed to read the user" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_users_id", "tags": [ "users" ], "summary": "Update a user (self-edit is always allowed; admin/active changes require admin; demoting or deactivating the last admin needs confirmDisableAuth)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the user (from GET /api/users)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "mfaEnabled": { "type": "boolean" }, "isAdmin": { "type": "boolean" }, "isActive": { "type": "boolean" }, "lastLogin": { "type": "string" }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "username", "mfaEnabled", "isAdmin", "isActive", "createdAt", "updatedAt" ] }, "example": { "id": 0, "username": "string", "email": "string", "displayName": "string", "mfaEnabled": true, "isAdmin": true, "isActive": true, "lastLogin": "string", "createdAt": "string", "updatedAt": "string" } } } }, "400": { "description": "User id is required, or the new password is shorter than 8 characters" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (editing another user without users:edit)" }, "404": { "description": "User not found" }, "409": { "description": "This is the last admin user (confirmDisableAuth required), or the username already exists" }, "500": { "description": "Failed to update the user" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "username": { "type": "string" }, "email": { "type": "string" }, "displayName": { "type": "string" }, "isAdmin": { "type": "boolean" }, "isActive": { "type": "boolean" }, "password": { "type": "string" }, "confirmDisableAuth": { "type": "boolean" } } }, "example": { "displayName": "Jane Doe", "email": "jane@example.com" } } } } }, "delete": { "operationId": "delete_api_users_id", "tags": [ "users" ], "summary": "Delete a user by id; deleting the last admin while auth is enabled disables authentication and requires confirmDisableAuth", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the user (from GET /api/users)" }, { "name": "confirmDisableAuth", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Set to true to confirm deleting the last admin (which disables authentication)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "authDisabled": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true, "authDisabled": true } } } }, "400": { "description": "User id is required" }, "401": { "description": "Authentication required (auth is enabled and the caller is not authenticated)" }, "403": { "description": "Permission denied (missing users:remove)" }, "404": { "description": "User not found" }, "409": { "description": "This is the last admin user — pass confirmDisableAuth=true to proceed" }, "500": { "description": "Failed to delete the user" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/users/{id}/mfa": { "post": { "operationId": "post_api_users_id_mfa", "tags": [ "users" ], "summary": "Set up or verify MFA for a user (action=setup regenerates a secret; action=verify confirms a code)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The user id" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Missing user id, missing/invalid MFA token, or an unknown action" }, "403": { "description": "Permission denied" }, "404": { "description": "User not found" }, "409": { "description": "MFA is already enabled (disable it before setting up again)" }, "500": { "description": "Failed to set up MFA" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "action": { "type": "string" }, "token": { "type": "string" } }, "required": [ "action" ] }, "example": { "action": "string", "token": "string" } } } } }, "delete": { "operationId": "delete_api_users_id_mfa", "tags": [ "users" ], "summary": "Disable MFA for a user", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The user id" } ], "responses": { "200": { "description": "Successful response" }, "400": { "description": "Missing user id" }, "403": { "description": "Permission denied" }, "404": { "description": "User not found" }, "500": { "description": "Failed to disable MFA" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/users/{id}/roles": { "get": { "operationId": "get_api_users_id_roles", "tags": [ "users" ], "summary": "List the roles assigned to a user (enterprise only)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the user (from GET /api/users)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "roleId": { "type": "integer" }, "name": { "type": "string" }, "environmentId": { "type": "integer" } }, "required": [ "roleId", "name" ] } }, "example": [ { "roleId": 0, "name": "string", "environmentId": 0 } ] } } }, "400": { "description": "User id is required" }, "403": { "description": "Enterprise license required" }, "404": { "description": "User not found" }, "500": { "description": "Failed to read the user roles" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_users_id_roles", "tags": [ "users" ], "summary": "Assign a role to a user, optionally scoped to an environment (enterprise, admin only)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the user (from GET /api/users)" } ], "responses": { "201": { "description": "The created user-role assignment" }, "400": { "description": "User id or role id is required" }, "403": { "description": "Enterprise license required, or admin access required" }, "404": { "description": "User not found" }, "500": { "description": "Failed to assign the role" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "roleId from GET /api/roles. environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "roleId": { "type": "integer" }, "environmentId": { "type": "integer" } }, "required": [ "roleId" ] }, "example": { "roleId": 3, "environmentId": 1 } } } } }, "delete": { "operationId": "delete_api_users_id_roles", "tags": [ "users" ], "summary": "Remove a role assignment from a user, optionally scoped to an environment (enterprise, admin only)", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Numeric id of the user (from GET /api/users)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "400": { "description": "User id or role id is required" }, "403": { "description": "Enterprise license required, or admin access required" }, "404": { "description": "Role assignment not found" }, "500": { "description": "Failed to remove the role" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "roleId from GET /api/roles. environmentId from GET /api/environments.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "roleId": { "type": "integer" }, "environmentId": { "type": "integer" } }, "required": [ "roleId" ] }, "example": { "roleId": 3, "environmentId": 1 } } } } } }, "/api/volumes": { "get": { "operationId": "get_api_volumes", "tags": [ "volumes" ], "summary": "List Docker volumes for an environment; returns an empty array when no environment is specified", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID to list volumes for (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "Name": { "type": "string" }, "Driver": { "type": "string" }, "Mountpoint": { "type": "string" }, "Scope": { "type": "string" }, "CreatedAt": { "type": "string" } }, "required": [ "Name", "Driver" ] } }, "example": [ { "Name": "web_data", "Driver": "local", "Mountpoint": "/var/lib/docker/volumes/web_data/_data", "Scope": "local", "CreatedAt": "2026-06-01T10:00:00Z" } ] } } }, "403": { "description": "Permission denied (requires volumes:view, or environment access denied on enterprise)" }, "404": { "description": "Environment not found" }, "500": { "description": "Failed to list volumes" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "post": { "operationId": "post_api_volumes", "tags": [ "volumes" ], "summary": "Create a Docker volume in an environment", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID to create the volume in (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "name": { "type": "string" } }, "required": [ "success", "name" ] }, "example": { "success": true, "name": "web_data" } } } }, "400": { "description": "Volume name is required" }, "403": { "description": "Permission denied (requires volumes:create, or environment access denied on enterprise)" }, "500": { "description": "Failed to create volume" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "driver": { "type": "string" }, "driverOpts": { "type": "object", "properties": {} }, "labels": { "type": "object", "properties": {} } }, "required": [ "name" ] }, "example": { "name": "web_data", "driver": "local", "driverOpts": {}, "labels": { "app": "web" } } } } } } }, "/api/volumes/{name}": { "get": { "operationId": "get_api_volumes_name", "tags": [ "volumes" ], "summary": "Inspect a single Docker volume by name (the name is validated as a Docker identifier)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker volume name (from GET /api/volumes)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "Name": { "type": "string" }, "Driver": { "type": "string" }, "Mountpoint": { "type": "string" }, "Scope": { "type": "string" }, "Labels": { "type": "object", "properties": {} }, "Options": { "type": "object", "properties": {} }, "CreatedAt": { "type": "string" } }, "required": [ "Name", "Driver", "Mountpoint" ] }, "example": { "Name": "web_data", "Driver": "local", "Mountpoint": "/var/lib/docker/volumes/web_data/_data", "Scope": "local", "Labels": {}, "Options": {}, "CreatedAt": "2026-06-01T10:00:00Z" } } } }, "403": { "description": "Permission denied (requires volumes:inspect, or environment access denied on enterprise)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "delete": { "operationId": "delete_api_volumes_name", "tags": [ "volumes" ], "summary": "Remove a Docker volume by name, optionally forcing removal", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker volume name (from GET /api/volumes)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" }, { "name": "force", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "Force removal of the volume" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (requires volumes:remove, or environment access denied on enterprise)" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/volumes/{name}/browse": { "get": { "operationId": "get_api_volumes_name_browse", "tags": [ "volumes" ], "summary": "Browse a directory inside a Docker volume via a cached helper container; the volume is mounted read-only when in use by other containers", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker volume name (from GET /api/volumes)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" }, { "name": "path", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Directory path inside the volume to list (defaults to \"/\")" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "path": { "type": "string" }, "entries": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "type": "string" }, "size": { "type": "integer" }, "permissions": { "type": "string" }, "owner": { "type": "string" }, "group": { "type": "string" }, "modified": { "type": "string" } }, "required": [ "name", "type", "size", "permissions", "owner", "group", "modified" ] } }, "usage": { "type": "array", "items": { "type": "object", "properties": { "containerId": { "type": "string" }, "containerName": { "type": "string" }, "state": { "type": "string" } }, "required": [ "containerId", "containerName", "state" ] } }, "isInUse": { "type": "boolean" }, "helperId": { "type": "string" } }, "required": [ "path", "entries", "usage", "isInUse", "helperId" ] }, "example": { "path": "/", "entries": [ { "name": "data", "type": "directory", "size": 4096, "permissions": "drwxr-xr-x", "owner": "root", "group": "root", "modified": "2026-06-01 10:00" } ], "usage": [], "isInUse": false, "helperId": "abc123" } } } }, "403": { "description": "Permission denied (requires volumes:inspect) or permission denied accessing the path" }, "404": { "description": "Directory not found" }, "500": { "description": "Failed to browse volume" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/volumes/{name}/browse/content": { "get": { "operationId": "get_api_volumes_name_browse_content", "tags": [ "volumes" ], "summary": "Read the content of a single file inside a Docker volume (files larger than 1MB are rejected)", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker volume name (from GET /api/volumes)" }, { "name": "path", "in": "query", "required": true, "schema": { "type": "string" }, "description": "File path inside the volume to read" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "content": { "type": "string" }, "path": { "type": "string" } }, "required": [ "content", "path" ] }, "example": { "content": "hello world\n", "path": "/data/readme.txt" } } } }, "400": { "description": "Path is required, or the path points to a directory" }, "403": { "description": "Permission denied (requires volumes:inspect) or permission denied reading the file" }, "404": { "description": "File not found" }, "413": { "description": "File is too large to view (max 1MB)" }, "500": { "description": "Failed to read file" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/volumes/{name}/browse/release": { "post": { "operationId": "post_api_volumes_name_browse_release", "tags": [ "volumes" ], "summary": "Release the cached helper container used to browse a Docker volume", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker volume name (from GET /api/volumes)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] }, "example": { "success": true } } } }, "403": { "description": "Permission denied (requires volumes:inspect)" }, "500": { "description": "Failed to release volume helper" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/volumes/{name}/clone": { "post": { "operationId": "post_api_volumes_name_clone", "tags": [ "volumes" ], "summary": "Clone a Docker volume into a new named volume, copying the data with a temporary helper container and preserving driver/options/labels", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Source Docker volume name (from GET /api/volumes)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "name": { "type": "string" } }, "required": [ "success", "name" ] }, "example": { "success": true, "name": "web_data_copy" } } } }, "400": { "description": "New volume name is required" }, "403": { "description": "Permission denied (requires volumes:create)" }, "500": { "description": "Failed to clone volume" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" } }, "required": [ "name" ] }, "example": { "name": "web_data_copy" } } } } } }, "/api/volumes/{name}/export": { "get": { "operationId": "get_api_volumes_name_export", "tags": [ "volumes" ], "summary": "Export a Docker volume (or a sub-path) as a downloadable archive — streaming tar, gzip-compressed tar.gz, or raw single-file bytes", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker volume name (from GET /api/volumes)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" }, { "name": "path", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Sub-path inside the volume to export (defaults to \"/\")" }, { "name": "format", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Archive format: \"tar\" (default), \"tar.gz\", or \"raw\" (single file)" } ], "responses": { "200": { "description": "Binary archive stream (Content-Disposition attachment); Content-Type is application/x-tar, application/gzip, or application/octet-stream depending on format" }, "403": { "description": "Permission denied (requires volumes:inspect)" }, "404": { "description": "Path not found" }, "500": { "description": "Failed to export volume" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/volumes/{name}/inspect": { "get": { "operationId": "get_api_volumes_name_inspect", "tags": [ "volumes" ], "summary": "Inspect a Docker volume by name, returning the raw Docker volume inspect object", "parameters": [ { "name": "name", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Docker volume name (from GET /api/volumes)" }, { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Environment ID the volume belongs to (from GET /api/environments)" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "Name": { "type": "string" }, "Driver": { "type": "string" }, "Mountpoint": { "type": "string" }, "Scope": { "type": "string" }, "Labels": { "type": "object", "properties": {} }, "Options": { "type": "object", "properties": {} }, "CreatedAt": { "type": "string" } }, "required": [ "Name", "Driver", "Mountpoint" ] }, "example": { "Name": "web_data", "Driver": "local", "Mountpoint": "/var/lib/docker/volumes/web_data/_data", "Scope": "local", "Labels": {}, "Options": {}, "CreatedAt": "2026-06-01T10:00:00Z" } } } }, "403": { "description": "Permission denied (requires volumes:inspect)" }, "500": { "description": "Failed to inspect volume" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/vulnerabilities": { "get": { "operationId": "get_api_vulnerabilities", "tags": [ "vulnerabilities" ], "summary": "A filtered, sorted page of aggregated vulnerability findings for an environment", "parameters": [], "responses": { "200": { "description": "A page of findings plus the filtered total count; a permission failure returns the status from the access check", "content": { "application/json": { "schema": { "type": "object", "properties": { "findings": { "type": "array", "items": { "type": "string" } }, "total": { "type": "integer" } }, "required": [ "findings", "total" ] }, "example": { "findings": [ { "cve": "CVE-2024-0001", "severity": "high", "package": "openssl", "imageName": "nginx:latest" } ], "total": 1 } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ], "description": "Accepts limit, offset, q, severity, image, container, stack, sort and dir query params (parsed centrally). Returns an empty page when no environment resolves." } }, "/api/vulnerabilities/count": { "get": { "operationId": "get_api_vulnerabilities_count", "tags": [ "vulnerabilities" ], "summary": "Return the total finding count, severity summary, and distinct image/container/stack filter values for an environment", "parameters": [], "responses": { "200": { "description": "An empty metadata object is returned when no environment resolves; a permission failure returns the status from the access check", "content": { "application/json": { "schema": { "type": "object", "properties": { "total": { "type": "integer" }, "summary": { "type": "string" }, "images": { "type": "array", "items": { "type": "string" } }, "containers": { "type": "array", "items": { "type": "string" } }, "stacks": { "type": "array", "items": { "type": "string" } } }, "required": [ "total", "summary", "images", "containers", "stacks" ] }, "example": { "total": 12, "summary": { "critical": 1, "high": 3, "medium": 5, "low": 3 }, "images": [ "nginx:latest" ], "containers": [ "web" ], "stacks": [ "frontend" ] } } } } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/vulnerabilities/export": { "get": { "operationId": "get_api_vulnerabilities_export", "tags": [ "vulnerabilities" ], "summary": "Export an environment's aggregated vulnerability findings as json or csv, matching the grid's filters and sort", "parameters": [ { "name": "format", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Output format — json (default) or csv" } ], "responses": { "200": { "description": "A permission failure returns the status from the access check; 500 is returned on an export error" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/api/vulnerabilities/scan-all": { "post": { "operationId": "post_api_vulnerabilities_scan-all", "tags": [ "vulnerabilities" ], "summary": "Scan every image in an environment for vulnerabilities, streaming per-image progress as Server-Sent Events", "parameters": [ { "name": "env", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "ID of the environment whose images to scan (from GET /api/environments)" } ], "responses": { "200": { "description": "A Server-Sent Events stream of progress and per-image results, ending with a summary \"result\" event" }, "403": { "description": "Permission denied, or (enterprise) no access to this environment" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/audit": { "get": { "operationId": "get_audit", "tags": [ "audit" ], "summary": "List audit log entries with optional filters (Enterprise only)", "parameters": [ { "name": "username", "in": "query", "required": false, "schema": { "type": "string" }, "description": "" }, { "name": "entity_type", "in": "query", "required": false, "schema": { "type": "string" }, "description": "" }, { "name": "action", "in": "query", "required": false, "schema": { "type": "string" }, "description": "" }, { "name": "environment_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "" }, { "name": "from_date", "in": "query", "required": false, "schema": { "type": "string" }, "description": "" }, { "name": "to_date", "in": "query", "required": false, "schema": { "type": "string" }, "description": "" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "" }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "logs": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "userId": { "type": "integer" }, "username": { "type": "string" }, "action": { "type": "string" }, "entityType": { "type": "string" }, "entityId": { "type": "string" }, "entityName": { "type": "string" }, "environmentId": { "type": "integer" }, "description": { "type": "string" }, "details": { "type": "object", "properties": {} }, "ipAddress": { "type": "string" }, "userAgent": { "type": "string" }, "createdAt": { "type": "string" } }, "required": [ "id", "username", "action", "entityType", "createdAt" ] } }, "total": { "type": "integer" }, "limit": { "type": "integer" }, "offset": { "type": "integer" } }, "required": [ "logs", "total", "limit", "offset" ] }, "example": { "logs": [ { "id": 0, "userId": 0, "username": "string", "action": "string", "entityType": "string", "entityId": "string", "entityName": "string", "environmentId": 0, "description": "string", "details": {}, "ipAddress": "string", "userAgent": "string", "createdAt": "string" } ], "total": 0, "limit": 0, "offset": 0 } } } }, "403": { "description": "Enterprise license required, or permission denied to view the audit log" }, "500": { "description": "Failed to fetch audit logs" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/audit/users": { "get": { "operationId": "get_audit_users", "tags": [ "audit" ], "summary": "List distinct usernames that appear in the audit log, for use as a filter dropdown (Enterprise only)", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } }, "example": [ "string" ] } } }, "403": { "description": "Enterprise license required, or permission denied to view the audit log" }, "500": { "description": "Failed to fetch users" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } }, "/metrics": { "get": { "operationId": "get_metrics", "tags": [ "metrics" ], "summary": "Prometheus metrics endpoint (disabled and 404s unless EXPORT_METRICS=true; public if app auth is disabled, otherwise requires a session or bearer token)", "parameters": [], "responses": { "200": { "description": "Prometheus text-exposition-format metrics body" }, "401": { "description": "Not authenticated (session or bearer token required when app auth is enabled)" }, "404": { "description": "Metrics export disabled (EXPORT_METRICS is not true)" }, "500": { "description": "Metrics collection failed" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] } } } }