{ "openapi": "3.0.3", "info": { "title": "Dockhand API", "version": "1.0.43", "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": "containers" }, { "name": "dashboard" }, { "name": "debug" }, { "name": "dependencies" }, { "name": "docs" }, { "name": "environments" }, { "name": "events" }, { "name": "git" }, { "name": "hawser" }, { "name": "health" }, { "name": "host" }, { "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": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "authEnabled": { "type": "boolean" }, "defaultProvider": { "type": "string" } } }, "example": { "authEnabled": true, "defaultProvider": "local" } } } } } }, "/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" }, "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" }, "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": {} } } }, "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/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}/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)", "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" } }, "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" }, { "id": 2, "name": "hhdocker02", "connectionType": "hawser-edge", "host": null, "port": 2375, "protocol": "http", "icon": "server", "publicIp": null, "timezone": "Europe/Berlin" } ] } } }, "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": "Successful response", "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" } }, "required": [ "id", "name", "connectionType" ] }, "example": { "id": 0, "name": "string", "connectionType": "string", "labels": [ "string" ], "publicIp": "string" } } } }, "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": "Successful response", "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" } }, "required": [ "id", "name", "connectionType" ] }, "example": { "id": 0, "name": "string", "connectionType": "string", "labels": [ "string" ], "publicIp": "string" } } } }, "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": [ "main", "develop", "feature/test" ] } } } }, "400": { "description": "The URL points at a loopback/link-local/metadata/reserved target, the credential does not match the URL host, 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). 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. A raw url may only be paired with a stored credentialId whose username plausibly matches that host (SSH credentials are always allowed). 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 credential does not match the URL host, 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; the raw url may only be paired with a stored credentialId whose username plausibly matches that host (exfiltration defense); 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" }, "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" }, "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" }, "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" }, "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, or the credential does not match the URL host" }, "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; the raw url may only be paired with a stored credentialId whose username plausibly matches that host (exfiltration defense).", "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": [] }, "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." } }, "/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/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 (non-secrets from file, secrets masked from DB) 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": "string" } }, "injectedSecretKeys": { "type": "array", "items": { "type": "string" } }, "secretProvider": { "type": "string" } }, "required": [ "variables" ] }, "example": { "variables": [ "string" ], "injectedSecretKeys": [ "string" ], "secretProvider": "string" } } } }, "403": { "description": "Permission denied (needs stacks:view)" }, "500": { "description": "Failed to read env vars" } }, "security": [ { "cookieAuth": [] }, { "bearerAuth": [] } ] }, "put": { "operationId": "put_api_stacks_name_env", "tags": [ "stacks" ], "summary": "Save a stack's secret env vars to the DB (secrets never hit disk)", "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": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "variables": { "type": "array", "items": { "type": "string" } } }, "required": [ "variables" ] }, "example": { "variables": [ "string" ] } } } } } }, "/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}/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) or recreate its containers (mode=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, anything else performs a plain 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": [], "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)", "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" } }, "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" }, "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" }, "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" }, "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": [] } ] } } } }