openapi: 3.1.0
info:
  title: PostGIS Service API
  version: 1.0.0
  description: |
    Source-backed contract for the PostGIS Service HTTP and WebSocket routes.
    All routes pass through Central session authentication and service authorization.
    The server may be mounted below BASE_PATH (commonly /postgis-service).
servers:
  - url: "{origin}{basePath}"
    variables:
      origin:
        default: http://localhost:8090
      basePath:
        default: ""
        description: Empty or the configured mount prefix, for example /postgis-service.
security:
  - centralSession: []
tags:
  - name: Browser
  - name: Connections
  - name: PostgreSQL roles
  - name: Network access
  - name: Settings
  - name: Databases
  - name: Jobs
  - name: Terminals
paths:
  /:
    get:
      tags: [Browser]
      operationId: redirectHome
      summary: Redirect to the databases page
      responses:
        "302":
          $ref: "#/components/responses/Redirect"
        "401":
          $ref: "#/components/responses/UnauthorizedText"
  /login:
    get:
      tags: [Browser]
      operationId: redirectLogin
      summary: Redirect to Central login
      parameters:
        - name: next
          in: query
          schema: {type: string}
      responses:
        "303":
          $ref: "#/components/responses/Redirect"
  /workspace:
    $ref: "#/components/pathItems/Page"
  /connections:
    $ref: "#/components/pathItems/Page"
  /databases:
    $ref: "#/components/pathItems/Page"
  /databases/{id}:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Browser]
      operationId: getDatabasePage
      summary: Render database detail page
      responses:
        "200": {$ref: "#/components/responses/HTML"}
    head:
      tags: [Browser]
      operationId: headDatabasePage
      summary: Read database detail page headers
      responses:
        "200": {$ref: "#/components/responses/HTML"}
  /jobs:
    $ref: "#/components/pathItems/Page"
  /users:
    $ref: "#/components/pathItems/Page"
  /users/detail:
    $ref: "#/components/pathItems/Page"
  /groups:
    $ref: "#/components/pathItems/Page"
  /groups/detail:
    $ref: "#/components/pathItems/Page"
  /network-access:
    $ref: "#/components/pathItems/Page"
  /settings:
    $ref: "#/components/pathItems/Page"
  /terminal:
    $ref: "#/components/pathItems/Page"
  /app.js:
    $ref: "#/components/pathItems/Asset"
  /style.css:
    $ref: "#/components/pathItems/Asset"
  /terminal.js:
    $ref: "#/components/pathItems/Asset"
  /shared/ui/{path}:
    parameters:
      - name: path
        in: path
        required: true
        description: Shared UI asset path. This template represents the router's path-prefix delegation.
        schema: {type: string}
    get:
      tags: [Browser]
      operationId: getSharedUIAsset
      summary: Serve a shared UI asset
      responses:
        "200": {description: Shared UI handler response}
        "404": {description: Asset not found}
  /api/connections:
    get:
      tags: [Connections]
      operationId: listConnections
      summary: List registered connections
      responses:
        "200":
          description: Connections
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/Connection"}
        "500": {$ref: "#/components/responses/Error"}
    post:
      tags: [Connections]
      operationId: createConnection
      summary: Register a PostgreSQL connection
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ConnectionCreate"}
      responses:
        "201": {$ref: "#/components/responses/IdCreated"}
        "400": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
        "500": {$ref: "#/components/responses/Error"}
  /api/connections/{id}:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Connections]
      operationId: getConnection
      summary: Get a registered connection
      responses:
        "200":
          description: Connection
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Connection"}
        "404": {$ref: "#/components/responses/Error"}
    put:
      tags: [Connections]
      operationId: updateConnection
      summary: Replace connection fields
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ConnectionWrite"}
      responses:
        "200": {$ref: "#/components/responses/Success"}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
    delete:
      tags: [Connections]
      operationId: deleteConnection
      summary: Delete connection metadata
      responses:
        "200": {$ref: "#/components/responses/Success"}
        "404": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/test:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Connections]
      operationId: testConnection
      summary: Test PostgreSQL connectivity
      responses:
        "200":
          description: Successful test
          content:
            application/json:
              schema:
                type: object
                required: [ok, server_version]
                properties:
                  ok: {const: true}
                  server_version: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/databases:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [Connections]
      operationId: listConnectionDatabases
      summary: List live databases on a connection
      responses:
        "200":
          description: Database names
          content:
            application/json:
              schema:
                type: object
                required: [databases]
                properties:
                  databases:
                    type: array
                    items: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/postgis-test:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Connections]
      operationId: testConnectionPostGIS
      summary: Test PostGIS in a database
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                database: {type: string, default: postgres}
      responses:
        "200":
          description: PostGIS status
          content:
            application/json:
              schema: {$ref: "#/components/schemas/PostGISTest"}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/hba-path:
    parameters: [{$ref: "#/components/parameters/Id"}]
    put:
      tags: [Network access]
      operationId: setConnectionHBAPath
      summary: Set a connection HBA file path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hba_file_path]
              properties:
                hba_file_path: {type: string}
      responses:
        "200":
          description: Stored path
          content:
            application/json:
              schema:
                type: object
                properties:
                  hba_file_path: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-users:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [PostgreSQL roles]
      operationId: listPGUsers
      summary: List PostgreSQL login roles
      responses:
        "200":
          description: Users
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/PGUser"}
        "400": {$ref: "#/components/responses/Error"}
    post:
      tags: [PostgreSQL roles]
      operationId: createPGUser
      summary: Create a PostgreSQL role
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/PGUserCreate"}
      responses:
        "201": {$ref: "#/components/responses/Name"}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-users/{name}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Name"
    put:
      tags: [PostgreSQL roles]
      operationId: updatePGUser
      summary: Update a PostgreSQL login role
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/PGUserUpdate"}
      responses:
        "200": {$ref: "#/components/responses/Name"}
        "400": {$ref: "#/components/responses/Error"}
    delete:
      tags: [PostgreSQL roles]
      operationId: deletePGUser
      summary: Drop a PostgreSQL login role
      responses:
        "200": {$ref: "#/components/responses/DeletedName"}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-users/{name}/access:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Name"
    get:
      tags: [PostgreSQL roles]
      operationId: listPGUserAccess
      summary: List a user's per-database access
      responses:
        "200": {$ref: "#/components/responses/AccessMatrix"}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-groups:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [PostgreSQL roles]
      operationId: listPGGroups
      summary: List PostgreSQL group roles
      responses:
        "200":
          description: Groups
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/PGGroup"}
        "400": {$ref: "#/components/responses/Error"}
    post:
      tags: [PostgreSQL roles]
      operationId: createPGGroup
      summary: Create a PostgreSQL group role
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string}
      responses:
        "201": {$ref: "#/components/responses/Name"}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-groups/{name}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Name"
    delete:
      tags: [PostgreSQL roles]
      operationId: deletePGGroup
      summary: Drop a PostgreSQL group role
      responses:
        "200": {$ref: "#/components/responses/DeletedName"}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-groups/{name}/access:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Name"
    get:
      tags: [PostgreSQL roles]
      operationId: listPGGroupAccess
      summary: List a group's per-database access
      responses:
        "200": {$ref: "#/components/responses/AccessMatrix"}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-groups/{name}/members:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Name"
    post:
      tags: [PostgreSQL roles]
      operationId: addPGGroupMember
      summary: Add a role to a PostgreSQL group
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [member]
              properties:
                member: {type: string}
      responses:
        "200":
          description: Membership
          content:
            application/json:
              schema:
                type: object
                properties:
                  group: {type: string}
                  member: {type: string}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/pg-groups/{name}/members/{member}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Name"
      - name: member
        in: path
        required: true
        schema: {type: string}
    delete:
      tags: [PostgreSQL roles]
      operationId: removePGGroupMember
      summary: Remove a role from a PostgreSQL group
      responses:
        "200":
          description: Removed member
          content:
            application/json:
              schema:
                type: object
                properties:
                  removed: {type: string}
        "400": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/access-rules:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [Network access]
      operationId: listAccessRules
      summary: List stored HBA rules
      responses:
        "200":
          description: Rules and effective path
          content:
            application/json:
              schema:
                type: object
                required: [rules, hba_path]
                properties:
                  rules:
                    type: array
                    items: {$ref: "#/components/schemas/HBARule"}
                  hba_path: {type: string}
        "500": {$ref: "#/components/responses/Error"}
    post:
      tags: [Network access]
      operationId: createAccessRule
      summary: Create an HBA rule
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/HBARuleWrite"}
      responses:
        "201": {$ref: "#/components/responses/IdCreated"}
        "400": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/access-rules/apply:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Network access]
      operationId: applyAccessRules
      summary: Write HBA rules and reload PostgreSQL
      responses:
        "200":
          description: Applied paths
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {const: applied}
                  backup_path: {type: string}
                  hba_path: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/connections/{id}/access-rules/{ruleId}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - name: ruleId
        in: path
        required: true
        schema: {type: integer}
    put:
      tags: [Network access]
      operationId: updateAccessRule
      summary: Replace an HBA rule
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/HBARuleWrite"}
      responses:
        "200": {$ref: "#/components/responses/Status"}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
    delete:
      tags: [Network access]
      operationId: deleteAccessRule
      summary: Delete an HBA rule
      responses:
        "200": {$ref: "#/components/responses/Status"}
        "404": {$ref: "#/components/responses/Error"}
  /api/settings:
    get:
      tags: [Settings]
      operationId: getSettings
      summary: Read curated PostgreSQL settings
      parameters:
        - $ref: "#/components/parameters/ConnectionIdQuery"
      responses:
        "200":
          description: Settings
          content:
            application/json:
              schema: {$ref: "#/components/schemas/SettingsResponse"}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/settings/update:
    post:
      tags: [Settings]
      operationId: updateSetting
      summary: Set one curated PostgreSQL setting
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/SettingUpdate"}
      responses:
        "200":
          description: Updated setting
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: {type: string}
                  value: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases:
    get:
      tags: [Databases]
      operationId: listDatabases
      summary: List registered databases
      parameters:
        - name: connection_id
          in: query
          schema: {type: integer}
      responses:
        "200":
          description: Databases
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/DatabaseListItem"}
        "500": {$ref: "#/components/responses/Error"}
    post:
      tags: [Databases]
      operationId: createDatabase
      summary: Queue database creation
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/DatabaseCreate"}
      responses:
        "202":
          description: Creation queued
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/QueuedJob"
                  - type: object
                    required: [database_id, message]
                    properties:
                      database_id: {type: integer}
                      message: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
        "500": {$ref: "#/components/responses/Error"}
  /api/databases/{id}:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [Databases]
      operationId: getDatabase
      summary: Get database details and spatial summary
      responses:
        "200":
          description: Database details
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DatabaseDetail"}
        "404": {$ref: "#/components/responses/Error"}
        "500": {$ref: "#/components/responses/Error"}
    delete:
      tags: [Databases]
      operationId: deleteDatabase
      summary: Queue database deletion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirm_name]
              properties:
                confirm_name: {type: string}
      responses:
        "202": {$ref: "#/components/responses/JobQueued"}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/clone:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Databases]
      operationId: cloneDatabase
      summary: Queue a same-connection database clone
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string}
      responses:
        "202":
          description: Clone queued
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/QueuedJob"
                  - type: object
                    properties:
                      database_id: {type: integer}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/backup:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Databases]
      operationId: backupDatabase
      summary: Queue a custom-format backup
      responses:
        "202": {$ref: "#/components/responses/JobQueued"}
        "500": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/backups:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [Databases]
      operationId: listDatabaseBackups
      summary: List backup artifacts
      responses:
        "200":
          description: Backup artifacts
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/BackupArtifact"}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/backups/{filename}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - name: filename
        in: path
        required: true
        schema: {type: string}
    get:
      tags: [Databases]
      operationId: downloadDatabaseBackup
      summary: Download a backup artifact
      responses:
        "200":
          description: Backup file
          headers:
            Content-Disposition:
              schema: {type: string}
          content:
            application/octet-stream:
              schema: {type: string, format: binary}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/restore:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Databases]
      operationId: restoreDatabase
      summary: Queue a database restore
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/RestoreRequest"}
      responses:
        "202": {$ref: "#/components/responses/JobQueued"}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/import:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Databases]
      operationId: importVector
      summary: Queue a vector import
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ImportRequest"}
      responses:
        "202": {$ref: "#/components/responses/JobQueued"}
        "400": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/import/upload:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Databases]
      operationId: uploadImportFile
      summary: Upload a vector source file
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
      responses:
        "200":
          description: Stored upload
          content:
            application/json:
              schema:
                type: object
                properties:
                  source_path: {type: string}
                  filename: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/schemas:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [Databases]
      operationId: listDatabaseSchemas
      summary: List non-system schemas
      responses:
        "200":
          description: Schemas
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required: [name]
                  properties:
                    name: {type: string}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/tables:
    parameters:
      - $ref: "#/components/parameters/Id"
      - name: schema
        in: query
        schema: {type: string}
    get:
      tags: [Databases]
      operationId: listDatabaseTables
      summary: List base tables and spatial metadata
      responses:
        "200":
          description: Tables
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/TableInfo"}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/access:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [PostgreSQL roles]
      operationId: listDatabaseAccess
      summary: List persisted database access grants
      responses:
        "200":
          description: Access grants
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/DatabaseAccess"}
        "400": {$ref: "#/components/responses/Error"}
    post:
      tags: [PostgreSQL roles]
      operationId: grantDatabaseAccess
      summary: Apply and persist a database access bundle
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/DatabaseAccessWrite"}
      responses:
        "200": {$ref: "#/components/responses/DatabaseAccessSet"}
        "400": {$ref: "#/components/responses/Error"}
  /api/databases/{id}/access/{name}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Name"
    put:
      tags: [PostgreSQL roles]
      operationId: updateDatabaseAccess
      summary: Update a database access bundle
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [level]
              properties:
                level: {$ref: "#/components/schemas/AccessLevel"}
      responses:
        "200": {$ref: "#/components/responses/DatabaseAccessSet"}
        "400": {$ref: "#/components/responses/Error"}
    delete:
      tags: [PostgreSQL roles]
      operationId: revokeDatabaseAccess
      summary: Revoke and delete a database access grant
      responses:
        "200":
          description: Revoked role
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked: {type: string}
        "400": {$ref: "#/components/responses/Error"}
  /api/jobs:
    get:
      tags: [Jobs]
      operationId: listJobs
      summary: List jobs newest first
      parameters:
        - name: limit
          in: query
          schema: {type: integer, default: 100}
      responses:
        "200":
          description: Jobs
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/Job"}
        "500": {$ref: "#/components/responses/Error"}
  /api/jobs/{id}:
    parameters: [{$ref: "#/components/parameters/Id"}]
    get:
      tags: [Jobs]
      operationId: getJob
      summary: Get one job
      responses:
        "200":
          description: Job
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "404": {$ref: "#/components/responses/Error"}
  /api/jobs/{id}/cancel:
    parameters: [{$ref: "#/components/parameters/Id"}]
    post:
      tags: [Jobs]
      operationId: cancelJob
      summary: Cancel a queued job
      responses:
        "200":
          description: Cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {const: cancelled}
        "404": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
  /api/terminal/ws/psql:
    get:
      tags: [Terminals]
      operationId: terminalWSPSQL
      summary: Upgrade to an interactive psql WebSocket
      x-websocket: true
      parameters:
        - $ref: "#/components/parameters/ConnectionIdQuery"
        - name: database
          in: query
          schema: {type: string, default: postgres}
      responses:
        "101": {$ref: "#/components/responses/WebSocketUpgrade"}
        "400": {$ref: "#/components/responses/BadRequestText"}
        "403": {$ref: "#/components/responses/ForbiddenText"}
  /api/terminal/ws/server:
    get:
      tags: [Terminals]
      operationId: terminalWSServer
      summary: Upgrade to the restricted server terminal WebSocket
      x-websocket: true
      parameters:
        - name: connection_id
          in: query
          schema: {type: integer}
        - name: database
          in: query
          schema: {type: string}
      responses:
        "101": {$ref: "#/components/responses/WebSocketUpgrade"}
        "400": {$ref: "#/components/responses/BadRequestText"}
        "403": {$ref: "#/components/responses/ForbiddenText"}
  /api/terminal/ws/logs:
    get:
      tags: [Terminals]
      operationId: terminalWSLogs
      summary: Upgrade to the PostgreSQL journal WebSocket
      x-websocket: true
      parameters:
        - name: lines
          in: query
          schema: {type: integer, default: 200}
      responses:
        "101": {$ref: "#/components/responses/WebSocketUpgrade"}
        "403": {$ref: "#/components/responses/ForbiddenText"}
components:
  securitySchemes:
    centralSession:
      type: apiKey
      in: cookie
      name: acugis_session
      description: Session created by Central. Bearer PATs are unsupported by current service wiring.
  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: {type: integer}
    Name:
      name: name
      in: path
      required: true
      schema: {type: string}
    ConnectionIdQuery:
      name: connection_id
      in: query
      required: true
      schema: {type: integer}
  pathItems:
    Page:
      get:
        tags: [Browser]
        summary: Render an HTML page
        responses:
          "200": {$ref: "#/components/responses/HTML"}
      head:
        tags: [Browser]
        summary: Read HTML page headers
        responses:
          "200": {$ref: "#/components/responses/HTML"}
    Asset:
      get:
        tags: [Browser]
        summary: Serve a web asset
        responses:
          "200": {description: Asset content}
          "404": {description: Asset not found}
      head:
        tags: [Browser]
        summary: Read web asset headers
        responses:
          "200": {description: Asset headers}
          "404": {description: Asset not found}
  responses:
    Error:
      description: JSON error
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    UnauthorizedText:
      description: Authentication required
      content:
        text/plain:
          schema: {type: string}
    ForbiddenText:
      description: Permission denied
      content:
        text/plain:
          schema: {type: string}
    BadRequestText:
      description: Invalid request before WebSocket upgrade
      content:
        text/plain:
          schema: {type: string}
    Redirect:
      description: Redirect
      headers:
        Location:
          required: true
          schema: {type: string, format: uri-reference}
    HTML:
      description: HTML document
      content:
        text/html:
          schema: {type: string}
    IdCreated:
      description: Created identifier
      content:
        application/json:
          schema:
            type: object
            required: [id]
            properties:
              id: {type: integer}
    Success:
      description: Successful mutation
      content:
        application/json:
          schema:
            type: object
            required: [success]
            properties:
              success: {type: boolean}
    Status:
      description: Mutation status
      content:
        application/json:
          schema:
            type: object
            properties:
              status: {type: string}
    Name:
      description: Role name
      content:
        application/json:
          schema:
            type: object
            properties:
              name: {type: string}
    DeletedName:
      description: Deleted role
      content:
        application/json:
          schema:
            type: object
            properties:
              deleted: {type: string}
    AccessMatrix:
      description: Per-database access
      content:
        application/json:
          schema:
            type: array
            items: {$ref: "#/components/schemas/RoleDatabaseAccess"}
    JobQueued:
      description: Job queued
      content:
        application/json:
          schema: {$ref: "#/components/schemas/QueuedJob"}
    DatabaseAccessSet:
      description: Applied access
      content:
        application/json:
          schema:
            type: object
            properties:
              role_name: {type: string}
              level: {$ref: "#/components/schemas/AccessLevel"}
    WebSocketUpgrade:
      description: Switching Protocols. Binary frames are terminal I/O; text frames are ControlMessage JSON.
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: {type: string}
    Connection:
      type: object
      required: [id, name, host, port, sslmode, admin_username, created_at, updated_at]
      properties:
        id: {type: integer}
        name: {type: string}
        host: {type: string}
        port: {type: integer}
        sslmode: {type: string}
        admin_username: {type: string}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    ConnectionWrite:
      type: object
      required: [name, host, admin_username]
      properties:
        name: {type: string}
        host: {type: string}
        port: {type: integer, default: 5432}
        sslmode: {type: string}
        admin_username: {type: string}
        admin_password:
          type: string
          writeOnly: true
          description: Required on create; blank retains the password on update.
    ConnectionCreate:
      allOf:
        - $ref: "#/components/schemas/ConnectionWrite"
        - type: object
          required: [admin_password]
    PostGISTest:
      type: object
      required: [ok, postgis_enabled, postgis_version, database]
      properties:
        ok: {const: true}
        postgis_enabled: {type: boolean}
        postgis_version: {type: string}
        database: {type: string}
    PGUser:
      type: object
      required: [name, can_login, is_superuser]
      properties:
        name: {type: string}
        can_login: {type: boolean}
        is_superuser: {type: boolean}
        groups: {type: array, items: {type: string}}
        valid_until: {type: [string, "null"], format: date-time}
    PGUserCreate:
      type: object
      required: [name, password]
      properties:
        name: {type: string}
        password: {type: string, writeOnly: true}
        can_login: {type: boolean, default: true}
        is_superuser: {type: boolean, default: false}
    PGUserUpdate:
      type: object
      properties:
        password: {type: string, writeOnly: true}
        can_login: {type: boolean}
        is_superuser: {type: boolean}
    PGGroup:
      type: object
      required: [name, can_login]
      properties:
        name: {type: string}
        members: {type: array, items: {type: string}}
        can_login: {type: boolean}
    AccessLevel:
      type: string
      enum: [readonly, readwrite, admin]
    DatabaseAccess:
      type: object
      properties:
        role_name: {type: string}
        is_group: {type: boolean}
        level: {$ref: "#/components/schemas/AccessLevel"}
        via_group: {type: string}
        database_id: {type: integer}
        database_name: {type: string}
        direct: {type: boolean}
    RoleDatabaseAccess:
      type: object
      required: [database_id, database_name, direct]
      properties:
        database_id: {type: integer}
        database_name: {type: string}
        level: {$ref: "#/components/schemas/AccessLevel"}
        via_group: {type: string}
        direct: {type: boolean}
        global_admin: {type: boolean}
    DatabaseAccessWrite:
      type: object
      required: [role_name, level]
      properties:
        role_name: {type: string}
        level: {$ref: "#/components/schemas/AccessLevel"}
    HBARule:
      allOf:
        - $ref: "#/components/schemas/HBARuleWrite"
        - type: object
          required: [id, connection_id, created_at, updated_at]
          properties:
            id: {type: integer}
            connection_id: {type: integer}
            created_at: {type: string, format: date-time}
            updated_at: {type: string, format: date-time}
    HBARuleWrite:
      type: object
      required: [name, cidr]
      properties:
        name: {type: string}
        cidr: {type: string}
        auth_method: {type: string, enum: [scram-sha-256, md5], default: scram-sha-256}
        database_name: {type: string}
        role_name: {type: string}
        enabled: {type: boolean, default: true}
    Setting:
      type: object
      required: [name, value, default_value, description, vartype, modified, pending_restart, restart_required, source, group]
      properties:
        name: {type: string}
        value: {type: string}
        default_value: {type: string}
        unit: {type: string}
        category: {type: string}
        description: {type: string}
        vartype: {type: string}
        enum_values: {type: array, items: {type: string}}
        modified: {type: boolean}
        pending_restart: {type: boolean}
        restart_required: {type: boolean}
        source: {type: string}
        group: {type: string}
    SettingsResponse:
      type: object
      required: [connection_id, restart_required_count, modified_count, remote_access_enabled, database_port, connection_registry_port, groups]
      properties:
        connection_id: {type: integer}
        restart_required_count: {type: integer}
        modified_count: {type: integer}
        remote_access_enabled: {type: boolean}
        database_port: {type: string}
        connection_registry_port: {type: integer}
        groups:
          type: array
          items:
            type: object
            required: [id, label, settings]
            properties:
              id: {type: string}
              label: {type: string}
              settings:
                type: array
                items: {$ref: "#/components/schemas/Setting"}
    SettingUpdate:
      type: object
      required: [connection_id, name, value]
      properties:
        connection_id: {type: integer}
        name: {type: string}
        value: {type: string}
    Database:
      type: object
      required: [id, connection_id, name, purpose, status, postgis_enabled, created_at, updated_at]
      properties:
        id: {type: integer}
        connection_id: {type: integer}
        name: {type: string}
        purpose: {type: string}
        status: {type: string}
        size_bytes: {type: [integer, "null"], format: int64}
        postgis_enabled: {type: boolean}
        database_role: {type: [string, "null"]}
        primary_database_id: {type: [integer, "null"]}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    DatabaseListItem:
      allOf:
        - $ref: "#/components/schemas/Database"
        - type: object
          properties:
            connection_name: {type: string}
            connection_host: {type: string}
            connection_port: {type: integer}
    DatabaseDetail:
      allOf:
        - $ref: "#/components/schemas/DatabaseListItem"
        - type: object
          required: [table_count, spatial_summary]
          properties:
            postgis_version: {type: string}
            table_count: {type: integer}
            spatial_summary: {$ref: "#/components/schemas/SpatialSummary"}
    SpatialSummary:
      type: object
      required: [spatial_table_count, geometry_types, srids]
      properties:
        spatial_table_count: {type: integer}
        geometry_types:
          type: array
          items:
            type: object
            properties:
              type: {type: string}
              count: {type: integer}
        srids:
          type: array
          items:
            type: object
            properties:
              srid: {type: integer}
              count: {type: integer}
    DatabaseCreate:
      type: object
      required: [connection_id, name]
      properties:
        connection_id: {type: integer}
        name: {type: string}
        purpose: {type: string, default: platform}
        enable_postgis: {type: boolean, default: true}
    BackupArtifact:
      type: object
      required: [artifact, size_bytes, filename]
      properties:
        artifact: {type: string}
        size_bytes: {type: integer, format: int64}
        filename: {type: string}
    RestoreRequest:
      type: object
      required: [artifact]
      properties:
        artifact: {type: string}
        mode: {type: string, enum: [existing, new], default: existing}
        new_database_name: {type: string}
    ImportRequest:
      type: object
      required: [source_path]
      properties:
        schema: {type: string, default: public}
        table_name: {type: string}
        source_path: {type: string}
        srid_policy: {type: string, examples: [preserve, detect, "EPSG:4326"]}
        layer_name: {type: string}
        overwrite: {type: boolean, default: true}
    TableInfo:
      type: object
      required: [schema, name]
      properties:
        schema: {type: string}
        name: {type: string}
        geometry_type: {type: string}
        srid: {type: integer}
        row_estimate: {type: integer, format: int64}
        size_bytes: {type: integer, format: int64}
        index_count: {type: integer}
    QueuedJob:
      type: object
      required: [job_id]
      properties:
        job_id: {type: integer}
    Job:
      type: object
      required: [id, type, status, progress, stage, payload, result, error_message, created_by, created_at]
      properties:
        id: {type: integer}
        type:
          type: string
          enum: [create_database, delete_database, clone_database, backup_database, restore_database, import_vector]
        status:
          type: string
          enum: [queued, running, completed, failed, cancelled]
        progress: {type: integer}
        stage: {type: string}
        payload: {type: object, additionalProperties: true}
        result: {type: object, additionalProperties: true}
        error_message: {type: string}
        created_by: {type: integer, format: int64}
        created_at: {type: string, format: date-time}
        started_at: {type: [string, "null"], format: date-time}
        finished_at: {type: [string, "null"], format: date-time}
    ControlMessage:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [resize, pause, resume, filter, ping, pong, error, info]
        cols: {type: integer}
        rows: {type: integer}
        filter: {type: string}
        message: {type: string}
