# Vendored from control-plane/internal/docs/openapi.yaml — regenerate with scripts/sync-openapi.* (do not edit by hand).
openapi: 3.0.3
info:
  title: Subnetly API
  version: v1
  description: |
    REST API for managing your Subnetly DNS, DHCP, and IPAM.

    ## Authentication
    Create an **API token** under **Settings → API Tokens** and send it as a
    bearer token on every request:

    ```
    Authorization: Bearer sbnt_your_token_here
    ```

    A token is bound to one tenant and carries a role chosen when you create it,
    so you do **not** need to send a tenant header — the tenant is implied by the
    token.

servers:
  - url: https://api.subnetly.com
    description: Subnetly API

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API token (sbnt_…)

  parameters:
    TenantID:
      name: X-Tenant-ID
      in: header
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the tenant to operate on.
    PathID:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
      required: [error]

    ApiToken:
      type: object
      description: A long-lived API token (the secret is never returned after creation).
      properties:
        id:         { type: string, format: uuid }
        name:       { type: string, example: ci-pipeline }
        role:       { type: string, enum: [owner, admin, engineer, viewer] }
        hint:       { type: string, example: "sbnt_1a2b3c…7f8e" }
        createdAt:  { type: string, format: date-time }
        lastUsedAt: { type: string, format: date-time, nullable: true }
        expiresAt:  { type: string, format: date-time, nullable: true }

    Tenant:
      type: object
      properties:
        id:      { type: string, format: uuid }
        name:    { type: string }
        slug:    { type: string }
        plan:    { type: string, example: starter }
        createdAt: { type: string, format: date-time }
      required: [id, name, slug, plan, createdAt]

    TenantRole:
      type: object
      properties:
        tenant: { $ref: '#/components/schemas/Tenant' }
        role:   { type: string, enum: [owner, admin, member, viewer] }
      required: [tenant, role]

    User:
      type: object
      properties:
        id:          { type: string, format: uuid }
        email:       { type: string, format: email }
        displayName: { type: string }
        createdAt:   { type: string, format: date-time }
      required: [id, email, displayName, createdAt]

    MeResponse:
      type: object
      properties:
        user:        { $ref: '#/components/schemas/User' }
        memberships: { type: array, items: { $ref: '#/components/schemas/TenantRole' } }
        isSuperuser: { type: boolean }
      required: [user, memberships, isSuperuser]

    FeaturePermissions:
      type: object
      properties:
        dns:    { type: string, enum: [none, view, edit] }
        dhcp:   { type: string, enum: [none, view, edit] }
        ipam:   { type: string, enum: [none, view, edit] }
        agents: { type: string, enum: [none, view, edit] }
        sites:  { type: string, enum: [none, view, edit] }

    MemberInfo:
      type: object
      properties:
        userId:             { type: string, format: uuid }
        email:              { type: string, format: email }
        displayName:        { type: string }
        role:               { type: string, enum: [owner, admin, member, viewer] }
        joinedAt:           { type: string, format: date-time }
        featurePermissions: { $ref: '#/components/schemas/FeaturePermissions' }
      required: [userId, email, role, joinedAt]

    Site:
      type: object
      properties:
        id:            { type: string, format: uuid }
        tenantId:      { type: string, format: uuid }
        name:          { type: string }
        region:        { type: string }
        parentId:      { type: string, format: uuid, nullable: true }
        inheritParent: { type: boolean }
        createdAt:     { type: string, format: date-time }
      required: [id, tenantId, name, region, createdAt]

    Agent:
      type: object
      properties:
        id:              { type: string, format: uuid }
        tenantId:        { type: string, format: uuid }
        siteId:          { type: string, format: uuid, nullable: true }
        name:            { type: string }
        status:          { type: string, enum: [pending, enrolled, connected, disconnected, error] }
        certFingerprint: { type: string, nullable: true }
        configVersion:   { type: integer }
        lastSeenAt:      { type: string, format: date-time, nullable: true }
        createdAt:       { type: string, format: date-time }
        dnsRole:         { type: string, enum: ['none', master, slave] }
        nameserverFqdn:  { type: string, nullable: true }
        publicIp:        { type: string, nullable: true }
        dnsRecursion:    { type: boolean }
        dnsAllowQuery:   { type: string, example: any }
        dnsAllowTransfer: { type: string, example: none }
        dnsForwarders:   { type: array, items: { type: string } }
        bindUp:          { type: boolean, nullable: true }
        keaUp:           { type: boolean, nullable: true }
      required: [id, tenantId, name, status, configVersion, createdAt]

    AgentEvent:
      type: object
      properties:
        id:        { type: string, format: uuid }
        agentId:   { type: string, format: uuid }
        eventType: { type: string }
        message:   { type: string }
        createdAt: { type: string, format: date-time }
      required: [id, agentId, eventType, createdAt]

    Zone:
      type: object
      properties:
        id:           { type: string, format: uuid }
        tenantId:     { type: string, format: uuid }
        siteId:       { type: string, format: uuid, nullable: true }
        name:         { type: string, example: example.com }
        zoneType:     { type: string, enum: [authoritative, reverse, forward] }
        serial:       { type: integer }
        dnssec:       { type: boolean }
        health:       { type: string, enum: [ok, warn, crit, unknown] }
        forwarders:   { type: array, items: { type: string } }
        primaryNs:    { type: string, nullable: true }
        allowQuery:   { type: string, example: any }
        allowTransfer: { type: string, example: none }
        soaRefresh:   { type: integer, example: 3600 }
        soaRetry:     { type: integer, example: 900 }
        soaExpire:    { type: integer, example: 604800 }
        soaMinimum:   { type: integer, example: 300 }
        soaRname:     { type: string, example: hostmaster }
        nameservers:  { type: array, items: { type: string } }
        recordCount:  { type: integer }
        createdAt:    { type: string, format: date-time }
        updatedAt:    { type: string, format: date-time }
      required: [id, tenantId, name, zoneType, serial, dnssec, health, recordCount, createdAt, updatedAt]

    DnsRecord:
      type: object
      properties:
        id:        { type: string, format: uuid }
        tenantId:  { type: string, format: uuid }
        zoneId:    { type: string, format: uuid }
        name:      { type: string, example: www }
        rtype:     { type: string, enum: [A, AAAA, CNAME, MX, TXT, NS, SRV, PTR, CAA, TLSA, SSHFP, NAPTR, CERT] }
        value:     { type: string }
        ttl:       { type: integer, example: 3600 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, zoneId, name, rtype, value, ttl, createdAt, updatedAt]

    DhcpScope:
      type: object
      properties:
        id:                      { type: string, format: uuid }
        tenantId:                { type: string, format: uuid }
        siteId:                  { type: string, format: uuid, nullable: true }
        subnetId:                { type: string, format: uuid, nullable: true }
        agentId:                 { type: string, format: uuid, nullable: true, description: 'Agent responsible for serving this scope. Null = unassigned.' }
        name:                    { type: string }
        cidr:                    { type: string, example: 10.10.1.0/24 }
        rangeStart:              { type: string, example: 10.10.1.10 }
        rangeEnd:                { type: string, example: 10.10.1.250 }
        health:                  { type: string, enum: [ok, warn, crit, unknown] }
        enabled:                 { type: boolean }
        reservationCount:        { type: integer }
        poolSize:                { type: integer }
        ddnsQualifyingSuffix:    { type: string }
        ddnsOverrideClientUpdate: { type: boolean }
        sharedNetwork:           { type: string }
        createdAt:               { type: string, format: date-time }
      required: [id, tenantId, name, cidr, rangeStart, rangeEnd, health, enabled, reservationCount, poolSize, createdAt]

    ScopeOption:
      type: object
      properties:
        code:  { type: integer, example: 3, description: 'RFC 2132 DHCP option code' }
        value: { type: string }
      required: [code, value]

    DhcpReservation:
      type: object
      properties:
        id:         { type: string, format: uuid }
        tenantId:   { type: string, format: uuid }
        scopeId:    { type: string, format: uuid }
        mac:        { type: string, example: 'AA:BB:CC:DD:EE:FF' }
        ip:         { type: string, example: 10.10.1.50 }
        hostname:   { type: string }
        clientId:   { type: string, description: 'DHCP option 61 client identifier' }
        bootFile:   { type: string, description: 'PXE boot file name (option 67)' }
        nextServer: { type: string, description: 'PXE next-server TFTP IP (option 66)' }
        createdAt:  { type: string, format: date-time }
      required: [id, tenantId, scopeId, mac, ip, createdAt]

    DhcpLease:
      type: object
      properties:
        tenantId:   { type: string, format: uuid }
        scopeId:    { type: string, format: uuid, nullable: true }
        ip:         { type: string }
        mac:        { type: string }
        hostname:   { type: string }
        lastAction: { type: string, enum: [add, del, expire, renew] }
        expiresAt:  { type: string, format: date-time, nullable: true }
        updatedAt:  { type: string, format: date-time }
      required: [ip, lastAction, updatedAt]

    LeaseSearchResult:
      type: object
      properties:
        recordType: { type: string, enum: [lease, reservation] }
        ip:         { type: string }
        mac:        { type: string }
        hostname:   { type: string }
        scopeId:    { type: string, format: uuid }
        scopeName:  { type: string }
        scopeCidr:  { type: string }
        expiresAt:  { type: string, format: date-time, nullable: true }
        lastAction: { type: string, nullable: true }
      required: [recordType, ip, mac, hostname, scopeId, scopeName, scopeCidr]

    HealthCheck:
      type: object
      properties:
        id:          { type: string, format: uuid }
        tenantId:    { type: string, format: uuid }
        checkType:   { type: string, example: agent_offline }
        severity:    { type: string, enum: [info, warn, critical] }
        entityType:  { type: string, example: agent }
        entityId:    { type: string, format: uuid, nullable: true }
        message:     { type: string }
        resolved:    { type: boolean }
        firstSeenAt: { type: string, format: date-time }
        lastSeenAt:  { type: string, format: date-time }
        resolvedAt:  { type: string, format: date-time, nullable: true }
      required: [id, tenantId, checkType, severity, entityType, message, resolved, firstSeenAt, lastSeenAt]

    SubnetDnsOptions:
      type: object
      properties:
        nameservers:   { type: array, items: { type: string } }
        searchDomains: { type: array, items: { type: string } }

    SubnetDhcpOptions:
      type: object
      properties:
        routers:    { type: array, items: { type: string } }
        dnsServers: { type: array, items: { type: string } }
        domainName: { type: string }
        ntpServers: { type: array, items: { type: string } }
        leaseTime:  { type: integer }

    IpamSubnet:
      type: object
      properties:
        id:          { type: string, format: uuid }
        tenantId:    { type: string, format: uuid }
        siteId:      { type: string, format: uuid, nullable: true }
        cidr:        { type: string, example: 10.10.0.0/24 }
        name:        { type: string }
        vlan:        { type: integer, nullable: true }
        parentId:    { type: string, format: uuid, nullable: true }
        isContainer: { type: boolean }
        gateway:     { type: string, nullable: true, example: 10.10.0.1 }
        vrf:         { type: string, nullable: true, example: VRF-CORP }
        attributes:  { type: object, additionalProperties: { type: string }, description: 'Custom key-value metadata' }
        dnsOptions:  { $ref: '#/components/schemas/SubnetDnsOptions' }
        dhcpOptions: { $ref: '#/components/schemas/SubnetDhcpOptions' }
        totalSize:   { type: integer, description: 'Number of usable IP addresses in this subnet' }
        usedCount:   { type: integer, description: 'Number of tracked IP addresses' }
        createdAt:   { type: string, format: date-time }
      required: [id, tenantId, cidr, name, isContainer, totalSize, usedCount, createdAt]

    DNSRef:
      type: object
      properties:
        id:       { type: string, format: uuid }
        name:     { type: string }
        zoneId:   { type: string, format: uuid }
        zoneName: { type: string }
      required: [id, name, zoneId, zoneName]

    DHCPRef:
      type: object
      properties:
        id:        { type: string, format: uuid }
        mac:       { type: string }
        scopeId:   { type: string, format: uuid }
        scopeName: { type: string }
      required: [id, mac, scopeId, scopeName]

    DhcpPool:
      type: object
      properties:
        scopeId:    { type: string, format: uuid }
        scopeName:  { type: string }
        rangeStart: { type: string }
        rangeEnd:   { type: string }
      required: [scopeId, scopeName, rangeStart, rangeEnd]

    IpamIPRecord:
      type: object
      properties:
        ip:               { type: string }
        subnetId:         { type: string, format: uuid }
        status:           { type: string, enum: [used, static, reserved, conflict] }
        hostname:         { type: string }
        mac:              { type: string }
        leaseExpiresAt:   { type: string, format: date-time, nullable: true }
        lastSeenAt:       { type: string, format: date-time, nullable: true }
        dnsRecords:       { type: array, items: { $ref: '#/components/schemas/DNSRef' } }
        dhcpReservations: { type: array, items: { $ref: '#/components/schemas/DHCPRef' } }
      required: [ip, subnetId, status, dnsRecords, dhcpReservations]

    DnsServerOptions:
      type: object
      properties:
        recursion:    { type: boolean }
        allowQuery:   { type: string, example: any }
        allowTransfer: { type: string, example: none }
        forwarders:   { type: array, items: { type: string } }
        rrl:          { type: boolean }
        rrlRate:      { type: integer }
        queryLog:     { type: boolean }
      required: [recursion, allowQuery, allowTransfer, forwarders, rrl, rrlRate, queryLog]

    DnsACL:
      type: object
      description: A named BIND acl{} block referenced by allow-query, allow-transfer, etc.
      properties:
        id:          { type: string, format: uuid }
        tenantId:    { type: string, format: uuid }
        name:        { type: string, example: trusted-nets }
        entries:     { type: array, items: { type: string }, example: ['10.0.0.0/8', '192.168.0.0/16'] }
        description: { type: string }
        createdAt:   { type: string, format: date-time }
        updatedAt:   { type: string, format: date-time }
      required: [id, tenantId, name, entries, createdAt, updatedAt]

    RPZZone:
      type: object
      description: A Response Policy Zone (DNS firewall) that groups override rules.
      properties:
        id:          { type: string, format: uuid }
        tenantId:    { type: string, format: uuid }
        name:        { type: string, example: block.internal }
        description: { type: string }
        priority:    { type: integer, description: 'Lower priority is evaluated first' }
        ruleCount:   { type: integer }
        createdAt:   { type: string, format: date-time }
      required: [id, tenantId, name, priority, ruleCount, createdAt]

    RPZRule:
      type: object
      properties:
        id:        { type: string, format: uuid }
        zoneId:    { type: string, format: uuid }
        trigger:   { type: string, example: malware.example.com, description: 'Query name the policy matches' }
        action:    { type: string, enum: [NXDOMAIN, NODATA, PASSTHRU, DROP], description: 'CNAME rewrite targets are also accepted as a raw value' }
        comment:   { type: string }
        createdAt: { type: string, format: date-time }
      required: [id, zoneId, trigger, action, createdAt]

    HAGroupMember:
      type: object
      properties:
        id:        { type: string, format: uuid }
        haGroupId: { type: string, format: uuid }
        agentId:   { type: string, format: uuid }
        agentName: { type: string }
        role:      { type: string, enum: [primary, standby, backup] }
        peerUrl:   { type: string, example: 'http://10.0.0.5:8000/', description: 'kea-ctrl-agent peer URL' }
      required: [id, haGroupId, agentId, role]

    HAGroup:
      type: object
      description: A Kea DHCP high-availability group (a pair of agents serving the same scopes).
      properties:
        id:                 { type: string, format: uuid }
        tenantId:           { type: string, format: uuid }
        name:               { type: string }
        mode:               { type: string, enum: [hot-standby, load-balancing, passive-backup] }
        heartbeatDelayMs:   { type: integer }
        maxResponseDelayMs: { type: integer }
        maxUnackedClients:  { type: integer }
        maxAckDelayMs:      { type: integer }
        members:            { type: array, items: { $ref: '#/components/schemas/HAGroupMember' } }
        createdAt:          { type: string, format: date-time }
      required: [id, tenantId, name, mode, createdAt]

    Webhook:
      type: object
      properties:
        id:        { type: string, format: uuid }
        tenantId:  { type: string, format: uuid }
        name:      { type: string }
        url:       { type: string, format: uri, example: 'https://hooks.example.com/subnetly' }
        events:    { type: array, items: { type: string }, description: 'Event types to deliver, e.g. dns.zone.created' }
        enabled:   { type: boolean }
        createdAt: { type: string, format: date-time }
      required: [id, tenantId, name, url, events, enabled, createdAt]

    WebhookDelivery:
      type: object
      properties:
        id:         { type: string, format: uuid }
        webhookId:  { type: string, format: uuid }
        eventType:  { type: string }
        statusCode: { type: integer, nullable: true }
        error:      { type: string, nullable: true }
        delivered:  { type: boolean }
        createdAt:  { type: string, format: date-time }
      required: [id, webhookId, eventType, delivered, createdAt]

    Notification:
      type: object
      properties:
        id:          { type: string, format: uuid }
        tenantId:    { type: string, format: uuid }
        userId:      { type: string, format: uuid, nullable: true }
        sourceType:  { type: string }
        sourceId:    { type: string, nullable: true }
        severity:    { type: string, enum: [info, warn, critical] }
        category:    { type: string }
        title:       { type: string }
        message:     { type: string }
        actionUrl:   { type: string, nullable: true }
        readAt:      { type: string, format: date-time, nullable: true }
        dismissedAt: { type: string, format: date-time, nullable: true }
        createdAt:   { type: string, format: date-time }
      required: [id, tenantId, severity, category, title, message, createdAt]

    NotificationPreferences:
      type: object
      properties:
        emailEnabled:              { type: boolean }
        inAppEnabled:              { type: boolean }
        criticalEmailImmediate:    { type: boolean }
        warnEmailDigest:           { type: boolean }
        infoEmailDigest:           { type: boolean }
        mobilePushEnabled:         { type: boolean }
        criticalPushEnabled:       { type: boolean }
        agentOfflinePushEnabled:   { type: boolean }
        agentRecoveredPushEnabled: { type: boolean }
        discoveryPushEnabled:      { type: boolean }
        billingPushEnabled:        { type: boolean }

    ImportJob:
      type: object
      description: Progress record for an asynchronous CSV import (DNS or IPAM).
      properties:
        id:        { type: string, format: uuid }
        kind:      { type: string, example: dns_records }
        status:    { type: string, enum: [running, succeeded, failed] }
        total:     { type: integer }
        created:   { type: integer }
        updated:   { type: integer }
        skipped:   { type: integer }
        failed:    { type: integer }
        error:     { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
      required: [id, kind, status]

    DnsView:
      type: object
      description: A BIND named view (split DNS) served by an agent.
      properties:
        id:           { type: string, format: uuid }
        agentId:      { type: string, format: uuid }
        name:         { type: string, example: internal }
        matchClients: { type: array, items: { type: string }, example: ['10.0.0.0/8'] }
        description:  { type: string }
        sortOrder:    { type: integer }
        createdAt:    { type: string, format: date-time }
        updatedAt:    { type: string, format: date-time }
      required: [id, agentId, name, createdAt]

    ZoneDeployment:
      type: object
      description: How a specific agent serves a DNS zone (role, NS/glue, transfer/query policy, view).
      properties:
        id:            { type: string, format: uuid, description: 'Zone ID' }
        name:          { type: string }
        role:          { type: string, enum: [primary, hidden-primary, secondary, forward, stub] }
        nsName:        { type: string, description: 'FQDN for the NS record; empty = none' }
        serviceIp:     { type: string, description: 'IP for glue A record; empty = use agent server config' }
        primaries:     { type: array, items: { type: string }, description: 'Master IPs (secondary/stub)' }
        alsoNotify:    { type: array, items: { type: string } }
        allowTransfer: { type: array, items: { type: string }, nullable: true }
        allowQuery:    { type: array, items: { type: string }, nullable: true }
        forwarders:    { type: array, items: { type: string } }
        forwardPolicy: { type: string, enum: [first, only] }
        viewId:        { type: string, format: uuid, description: 'Empty = flat mode' }
      required: [id, name, role]

    ScopeDeployment:
      type: object
      description: How a specific agent serves a DHCP scope.
      properties:
        id:                       { type: string, format: uuid, description: 'Scope ID' }
        name:                     { type: string }
        cidr:                     { type: string }
        rangeStart:               { type: string }
        rangeEnd:                 { type: string }
        ddnsQualifyingSuffix:     { type: string }
        ddnsOverrideClientUpdate: { type: boolean }
        sharedNetwork:            { type: string }
        options:                  { type: array, items: { $ref: '#/components/schemas/ScopeOption' } }
        haGroupId:                { type: string, format: uuid, description: 'Empty = no HA' }
      required: [id, name, cidr]

    AgentCommand:
      type: object
      description: A control command dispatched to an agent (update, log fetch, etc.).
      properties:
        id:          { type: string, format: uuid }
        agentId:     { type: string, format: uuid }
        type:        { type: string, example: fetch_logs }
        payload:     { type: object, nullable: true }
        status:      { type: string, enum: [pending, claimed, running, succeeded, failed, expired] }
        requestedBy: { type: string }
        result:      { type: string }
        error:       { type: string }
        output:      { type: object, nullable: true }
        requestedAt: { type: string, format: date-time }
        claimedAt:   { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
        expiresAt:   { type: string, format: date-time }
      required: [id, agentId, type, status, requestedAt, expiresAt]

    AgentTelemetry:
      type: object
      description: Composed live view of an agent — status, latest heartbeat/metrics, and scaling recommendations.
      properties:
        agentId:         { type: string, format: uuid }
        status:          { type: string, enum: [online, degraded, offline, updating, error] }
        lastSeenAt:      { type: string, format: date-time, nullable: true }
        heartbeat:       { type: object, nullable: true }
        metrics:         { type: object, nullable: true }
        assessment:      { type: string, enum: [healthy, degraded, overloaded, critical] }
        recommendations: { type: array, items: { type: object } }
      required: [agentId, status]

paths:

  /v1/health-checks:
    get:
      summary: List active tenant health checks
      operationId: listHealthChecks
      tags: [Observability]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Active unresolved health checks
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/HealthCheck' }

  # ── Auth / identity ─────────────────────────────────────────────────────────

  /v1/me:
    get:
      summary: Get current user and tenant memberships
      operationId: getMe
      tags: [Auth]
      responses:
        '200':
          description: Current user
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MeResponse' }
        '401':
          description: Unauthenticated

  /v1/api-tokens:
    get:
      summary: List your API tokens for the active tenant
      operationId: listApiTokens
      tags: [API Tokens]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
      responses:
        '200':
          description: Tokens
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokens:
                    type: array
                    items: { $ref: '#/components/schemas/ApiToken' }
    post:
      summary: Create a long-lived API token (secret shown once)
      operationId: createApiToken
      description: |
        Create this from the Subnetly console. The role is capped at your own
        role; the secret is returned only in this response.
      tags: [API Tokens]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, role]
              properties:
                name: { type: string, example: ci-pipeline }
                role: { type: string, enum: [owner, admin, engineer, viewer] }
                expiresInDays: { type: integer, description: '0 or omitted uses the tenant default expiry policy', example: 90 }
      responses:
        '201':
          description: Created — plaintext secret returned once
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { $ref: '#/components/schemas/ApiToken' }
                  plaintext: { type: string, example: "sbnt_xxxxxxxxxxxxxxxx" }
        '403':
          description: Role exceeds your access, or the request used an API token

  /v1/api-tokens/{id}:
    delete:
      summary: Revoke an API token
      operationId: revokeApiToken
      tags: [API Tokens]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Revoked
        '404':
          description: Not found

  /v1/settings:
    get:
      summary: Get tenant settings
      operationId: getTenantSettings
      tags: [Settings]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
      responses:
        '200':
          description: Tenant settings, security policy, billing metadata, and audit retention controls
          content:
            application/json:
              schema: { type: object }
    patch:
      summary: Update tenant settings
      operationId: updateTenantSettings
      tags: [Settings]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        '200':
          description: Updated tenant settings
          content:
            application/json:
              schema: { type: object }

  /v1/admin/audit:
    get:
      summary: List tenant audit events visible to the caller
      operationId: listAuditEvents
      tags: [Audit]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
        - { name: kind, in: query, schema: { type: string } }
        - { name: action, in: query, schema: { type: string } }
        - { name: actor, in: query, schema: { type: string } }
        - { name: q, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 500 } }
      responses:
        '200':
          description: Audit events
          content:
            application/json:
              schema: { type: object }

  /v1/admin/audit/export:
    get:
      summary: Export tenant audit events
      operationId: exportAuditEvents
      tags: [Audit]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
        - { name: format, in: query, schema: { type: string, enum: [csv, json] } }
      responses:
        '200':
          description: CSV or JSON audit export

  /v1/audit/{entryId}/revert:
    post:
      summary: Revert a supported audited change
      operationId: revertAuditEntry
      tags: [Audit]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
        - { name: entryId, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Revert completed
          content:
            application/json:
              schema: { type: object }
        '400':
          description: Audit entry is not revertible

  /v1/admin/export:
    get:
      summary: Export all tenant data (DNS, DHCP, IPAM, sites, agents)
      operationId: exportTenantData
      tags: [Admin]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: JSON export of the tenant's managed resources
          content:
            application/json:
              schema: { type: object }

  /v1/msp/activity:
    get:
      summary: List MSP activity across authorized client tenants
      operationId: listMspActivity
      tags: [MSP]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
        - { name: clientId, in: query, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer, maximum: 500 } }
      responses:
        '200':
          description: MSP activity feed
          content:
            application/json:
              schema: { type: object }

  /v1/msp/clients/{clientId}/monthly-report:
    post:
      summary: Generate a monthly MSP client report
      operationId: generateMspMonthlyReport
      tags: [MSP]
      parameters:
        - { $ref: '#/components/parameters/TenantID' }
        - { name: clientId, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Generated monthly report markdown
          content:
            application/json:
              schema: { type: object }

  /v1/tenants:
    get:
      summary: List tenants for the current user
      operationId: listTenants
      tags: [Auth]
      responses:
        '200':
          description: Tenants list
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenants:
                    type: array
                    items: { $ref: '#/components/schemas/TenantRole' }

  /v1/profile:
    put:
      summary: Update the current user's profile
      operationId: updateProfile
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                firstName:   { type: string }
                lastName:    { type: string }
                displayName: { type: string }
      responses:
        '200':
          description: Updated user
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }

  /v1/profile/tenant-summary:
    get:
      summary: Get the current tenant plus usage counts for the caller
      operationId: getProfileTenantSummary
      tags: [Auth]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Tenant and usage summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenant: { $ref: '#/components/schemas/Tenant' }
                  usage:  { type: object }

  /v1/account:
    delete:
      summary: Delete the current user's own account
      description: |
        Self-service account deletion (required by the App Store). Owned tenants
        enter a 30-day grace period before permanent deletion; recovery links are
        emailed. The only remaining superadmin cannot self-delete.
      operationId: deleteOwnAccount
      tags: [Account]
      responses:
        '200':
          description: Deletion scheduled
          content:
            application/json:
              schema: { type: object }
        '403':
          description: The only superadmin account cannot self-delete
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  # ── Tenant admin (member management within caller's own tenant) ─────────────

  /v1/admin/members:
    get:
      summary: List members of the caller''s tenant
      operationId: adminListMembers
      tags: [Admin]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Members
          content:
            application/json:
              schema:
                type: object
                properties:
                  members: { type: array, items: { $ref: '#/components/schemas/MemberInfo' } }
    post:
      summary: Invite a member to the caller''s tenant
      operationId: adminInviteMember
      tags: [Admin]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:       { type: string, format: email }
                displayName: { type: string }
                role:        { type: string, enum: [admin, member, viewer] }
              required: [email, role]
      responses:
        '201':
          description: Member invited
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MemberInfo' }

  /v1/admin/members/{userId}:
    parameters:
      - name: userId
        in: path
        required: true
        schema: { type: string, format: uuid }
    put:
      summary: Update member role
      operationId: adminUpdateMemberRole
      tags: [Admin]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                role: { type: string, enum: [admin, member, viewer] }
              required: [role]
      responses:
        '204': { description: Updated }
    delete:
      summary: Remove member from tenant
      operationId: adminRemoveMember
      tags: [Admin]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Removed }

  /v1/admin/members/{userId}/permissions:
    parameters:
      - name: userId
        in: path
        required: true
        schema: { type: string, format: uuid }
    put:
      summary: Set feature-level permissions for a member
      operationId: adminSetPermissions
      tags: [Admin]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FeaturePermissions' }
      responses:
        '204': { description: Updated }

  # ── Webhooks ─────────────────────────────────────────────────────────────────

  /v1/webhooks:
    get:
      summary: List webhooks
      operationId: listWebhooks
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks: { type: array, items: { $ref: '#/components/schemas/Webhook' } }
    post:
      summary: Create a webhook (signing secret returned once)
      operationId: createWebhook
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:    { type: string }
                url:     { type: string, format: uri }
                events:  { type: array, items: { type: string } }
                enabled: { type: boolean }
              required: [name, url]
      responses:
        '201':
          description: Created — the plaintext signing secret is returned only here
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook: { $ref: '#/components/schemas/Webhook' }
                  secret:  { type: string, description: 'HMAC signing secret (shown once)' }

  /v1/webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Update a webhook
      operationId: updateWebhook
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:    { type: string }
                url:     { type: string, format: uri }
                events:  { type: array, items: { type: string } }
                enabled: { type: boolean }
      responses:
        '200':
          description: Updated webhook
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Webhook' }
    delete:
      summary: Delete a webhook
      operationId: deleteWebhook
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/webhooks/{id}/test:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Send a test event to a webhook
      operationId: testWebhook
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Delivery attempt result
          content:
            application/json:
              schema: { type: object }

  /v1/webhooks/{id}/deliveries:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List recent delivery attempts for a webhook
      operationId: listWebhookDeliveries
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Recent deliveries
          content:
            application/json:
              schema:
                type: object
                properties:
                  deliveries: { type: array, items: { $ref: '#/components/schemas/WebhookDelivery' } }

  # ── Notifications ────────────────────────────────────────────────────────────

  /v1/notifications:
    get:
      summary: List notifications for the current user
      operationId: listNotifications
      tags: [Notifications]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - { name: includeRead, in: query, schema: { type: boolean } }
        - { name: severity, in: query, schema: { type: string, enum: [info, warn, critical] } }
        - { name: category, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 200 } }
      responses:
        '200':
          description: Notifications
          content:
            application/json:
              schema:
                type: object
                properties:
                  notifications: { type: array, items: { $ref: '#/components/schemas/Notification' } }

  /v1/notifications/unread-count:
    get:
      summary: Get the unread notification count
      operationId: getUnreadNotificationCount
      tags: [Notifications]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Unread count
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }

  /v1/notifications/mark-all-read:
    post:
      summary: Mark all notifications as read
      operationId: markAllNotificationsRead
      tags: [Notifications]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Marked }

  /v1/notifications/{id}/read:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Mark a notification as read
      operationId: markNotificationRead
      tags: [Notifications]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Marked }

  /v1/notifications/{id}/dismiss:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Dismiss a notification
      operationId: dismissNotification
      tags: [Notifications]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Dismissed }

  /v1/notification-preferences:
    get:
      summary: Get the current user's notification preferences
      operationId: getNotificationPreferences
      tags: [Notifications]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Preferences
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationPreferences' }
    put:
      summary: Update the current user's notification preferences
      operationId: updateNotificationPreferences
      tags: [Notifications]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NotificationPreferences' }
      responses:
        '200':
          description: Updated preferences
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationPreferences' }

  # ── Sites ───────────────────────────────────────────────────────────────────

  /v1/sites:
    get:
      summary: List sites
      operationId: listSites
      tags: [Sites]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Sites
          content:
            application/json:
              schema:
                type: object
                properties:
                  sites: { type: array, items: { $ref: '#/components/schemas/Site' } }
    post:
      summary: Create a site
      operationId: createSite
      tags: [Sites]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:          { type: string }
                region:        { type: string }
                parentId:      { type: string, format: uuid, nullable: true }
                inheritParent: { type: boolean }
              required: [name, region]
      responses:
        '201':
          description: Site created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Site' }

  /v1/sites/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Update a site
      operationId: updateSite
      tags: [Sites]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:          { type: string }
                region:        { type: string }
                parentId:      { type: string, format: uuid, nullable: true }
                inheritParent: { type: boolean }
              required: [name, region]
      responses:
        '200':
          description: Updated site
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Site' }
    delete:
      summary: Delete a site
      operationId: deleteSite
      tags: [Sites]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  # ── Agents ──────────────────────────────────────────────────────────────────

  /v1/agents:
    get:
      summary: List agents
      operationId: listAgents
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  agents: { type: array, items: { $ref: '#/components/schemas/Agent' } }
    post:
      summary: Create an agent (generates a bootstrap token)
      operationId: createAgent
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
              required: [name]
      responses:
        '201':
          description: Agent created with enrollment token
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent:      { $ref: '#/components/schemas/Agent' }
                  token:      { type: string, description: 'One-time enrollment token (shown once)' }
                  agentImage: { type: string, description: 'Container registry image URI to deploy' }

  /v1/agents/enroll:
    post:
      summary: Enroll an agent using a bootstrap token and CSR
      operationId: enrollAgent
      security: []
      tags: [Agents]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                token: { type: string }
                csr:   { type: string, description: 'PEM-encoded PKCS#10 CSR' }
              required: [token, csr]
      responses:
        '200':
          description: Enrollment result — returns signed certificate and bootstrap configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  cert:       { type: string, description: 'PEM-encoded signed agent certificate' }
                  caCert:     { type: string, description: 'PEM-encoded CA certificate' }
                  grpcAddr:   { type: string }
                  tenantId:   { type: string, format: uuid }
                  agentId:    { type: string, format: uuid }

  /v1/agents/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    delete:
      summary: Delete an agent
      operationId: deleteAgent
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/agents/{id}/site:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Assign an agent to a site
      operationId: assignAgentSite
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                siteId: { type: string, format: uuid, nullable: true }
              required: [siteId]
      responses:
        '200':
          description: Updated agent
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }

  /v1/agents/{id}/role:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Update DNS server role and named.conf options for an agent
      operationId: updateAgentRole
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                dnsRole:         { type: string, enum: ['none', master, slave] }
                nameserverFqdn:  { type: string, nullable: true }
                publicIp:        { type: string, nullable: true }
                dnsRecursion:    { type: boolean }
                dnsAllowQuery:   { type: string }
                dnsAllowTransfer: { type: string }
                dnsForwarders:   { type: array, items: { type: string } }
              required: [dnsRole]
      responses:
        '200':
          description: Updated agent
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }

  /v1/agents/{id}/events:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get recent events for an agent
      operationId: listAgentEvents
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Events (most recent 100)
          content:
            application/json:
              schema:
                type: object
                properties:
                  events: { type: array, items: { $ref: '#/components/schemas/AgentEvent' } }

  /v1/agents/{id}/reissue:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Reissue a bootstrap token for an agent
      operationId: reissueAgentToken
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: New token
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:      { type: string }
                  agentImage: { type: string }

  /v1/agents/{id}/cert-rotate:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Rotate agent mTLS certificate
      operationId: rotateAgentCert
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                csr: { type: string, description: 'PEM-encoded PKCS#10 CSR' }
              required: [csr]
      responses:
        '200':
          description: New signed certificate
          content:
            application/json:
              schema:
                type: object
                properties:
                  cert:   { type: string }
                  caCert: { type: string }

  /v1/agents/{id}/inventory:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get the DNS/DHCP inventory an agent currently serves
      operationId: getAgentInventory
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Agent inventory (zones, scopes, records)
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/sync-windows-inventory:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Trigger a Windows Server inventory sync on the agent
      operationId: syncAgentWindowsInventory
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Sync requested
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/migration-preview:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Preview what would be imported from a Windows/legacy agent
      operationId: agentMigrationPreview
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Migration preview
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/migrate:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Execute a migration/import from an agent's discovered config
      operationId: executeAgentMigration
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object }
      responses:
        '200':
          description: Migration result
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/telemetry:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get live telemetry, status, and scaling recommendations for an agent
      operationId: getAgentTelemetry
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Agent telemetry
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentTelemetry' }

  /v1/agents/{id}/commands:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List control commands dispatched to an agent
      operationId: listAgentCommands
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Commands
          content:
            application/json:
              schema:
                type: object
                properties:
                  commands: { type: array, items: { $ref: '#/components/schemas/AgentCommand' } }

  /v1/agents/{id}/commands/{cmdId}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: cmdId, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      summary: Get a single agent command (poll for its result)
      operationId: getAgentCommand
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Command
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentCommand' }
        '404':
          description: Not found

  /v1/agents/{id}/update:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Request an agent self-update
      operationId: requestAgentUpdate
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                version: { type: string, description: 'Target version; omit for latest' }
      responses:
        '200':
          description: Update command queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentCommand' }

  /v1/agents/{id}/log-sources:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List log sources available on an agent
      operationId: listAgentLogSources
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Log sources
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/logs:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Request a tail of agent logs (async — poll the returned command)
      operationId: requestAgentLogs
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                source: { type: string, description: 'Log source key (e.g. bind, kea, agent)' }
                tail:   { type: integer, description: 'Number of trailing lines (default 200)' }
                since:  { type: string, format: date-time, description: 'Only lines after this time' }
                filter: { type: string, description: 'Literal substring filter' }
              required: [source]
      responses:
        '200':
          description: Log-fetch command queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentCommand' }

  /v1/agents/{id}/dns-config:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get the per-agent DNS server (named.conf) configuration
      operationId: getAgentDnsConfig
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Agent DNS config
          content:
            application/json:
              schema: { type: object }
    put:
      summary: Update the per-agent DNS server configuration
      operationId: setAgentDnsConfig
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                recursion:     { type: boolean }
                allowQuery:    { type: array, items: { type: string } }
                allowTransfer: { type: array, items: { type: string } }
                allowRecursion: { type: array, items: { type: string } }
                forwarders:    { type: array, items: { type: string } }
                dnssecValidation: { type: boolean }
      responses:
        '200':
          description: Updated config
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/dhcp-config:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get the per-agent DHCP (Kea) server configuration
      operationId: getAgentDhcpConfig
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Agent DHCP config
          content:
            application/json:
              schema: { type: object }
    put:
      summary: Update the per-agent DHCP server configuration
      operationId: setAgentDhcpConfig
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        '200':
          description: Updated config
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/zone-authority:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get which zones this agent is authoritative for
      operationId: getAgentZoneAuthority
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Zone authority summary
          content:
            application/json:
              schema: { type: object }

  /v1/agents/{id}/zone-deployments:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List zone deployments served by an agent
      operationId: listAgentZoneDeployments
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Zone deployments
          content:
            application/json:
              schema:
                type: object
                properties:
                  deployments: { type: array, items: { $ref: '#/components/schemas/ZoneDeployment' } }
    post:
      summary: Deploy a zone to an agent
      operationId: createAgentZoneDeployment
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                zoneId:        { type: string, format: uuid }
                role:          { type: string, enum: [primary, hidden-primary, secondary, forward, stub] }
                nsName:        { type: string }
                serviceIp:     { type: string }
                primaries:     { type: array, items: { type: string } }
                alsoNotify:    { type: array, items: { type: string } }
                allowTransfer: { type: array, items: { type: string } }
                allowQuery:    { type: array, items: { type: string } }
                forwarders:    { type: array, items: { type: string } }
                forwardPolicy: { type: string, enum: [first, only] }
                viewId:        { type: string, format: uuid }
              required: [zoneId, role]
      responses:
        '201':
          description: Zone deployment created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ZoneDeployment' }

  /v1/agents/{id}/zone-deployments/{depId}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: depId, in: path, required: true, schema: { type: string, format: uuid } }
    put:
      summary: Update a zone deployment on an agent
      operationId: updateAgentZoneDeployment
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ZoneDeployment' }
      responses:
        '200':
          description: Updated deployment
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ZoneDeployment' }
    delete:
      summary: Remove a zone deployment from an agent
      operationId: deleteAgentZoneDeployment
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/agents/{id}/scope-deployments:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List DHCP scope deployments served by an agent
      operationId: listAgentScopeDeployments
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Scope deployments
          content:
            application/json:
              schema:
                type: object
                properties:
                  deployments: { type: array, items: { $ref: '#/components/schemas/ScopeDeployment' } }
    post:
      summary: Deploy a DHCP scope to an agent
      operationId: createAgentScopeDeployment
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                scopeId:   { type: string, format: uuid }
                haGroupId: { type: string, format: uuid, description: 'Optional HA group' }
              required: [scopeId]
      responses:
        '201':
          description: Scope deployment created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScopeDeployment' }

  /v1/agents/{id}/scope-deployments/{depId}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: depId, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      summary: Remove a DHCP scope deployment from an agent
      operationId: deleteAgentScopeDeployment
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/agents/{id}/dns-views:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List BIND views (split DNS) on an agent
      operationId: listAgentDnsViews
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: DNS views
          content:
            application/json:
              schema:
                type: object
                properties:
                  views: { type: array, items: { $ref: '#/components/schemas/DnsView' } }
    post:
      summary: Create a BIND view on an agent
      operationId: createAgentDnsView
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:         { type: string }
                matchClients: { type: array, items: { type: string } }
                description:  { type: string }
                sortOrder:    { type: integer }
              required: [name]
      responses:
        '201':
          description: View created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsView' }

  /v1/agents/{id}/dns-views/{viewId}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: viewId, in: path, required: true, schema: { type: string, format: uuid } }
    put:
      summary: Update a BIND view
      operationId: updateAgentDnsView
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:         { type: string }
                matchClients: { type: array, items: { type: string } }
                description:  { type: string }
                sortOrder:    { type: integer }
      responses:
        '200':
          description: Updated view
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsView' }
    delete:
      summary: Delete a BIND view
      operationId: deleteAgentDnsView
      tags: [Agents]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  # ── DNS server-level options ─────────────────────────────────────────────────

  /v1/dns/server-options:
    get:
      summary: Get DNS server options (global or per-site)
      operationId: getDnsServerOptions
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - name: siteId
          in: query
          schema: { type: string, format: uuid }
          description: Omit for global defaults
      responses:
        '200':
          description: Server options
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsServerOptions' }
    put:
      summary: Set DNS server options
      operationId: setDnsServerOptions
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - name: siteId
          in: query
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DnsServerOptions' }
      responses:
        '200':
          description: Updated options
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsServerOptions' }

  # ── DNS zones ────────────────────────────────────────────────────────────────

  /v1/dns/zones:
    get:
      summary: List DNS zones
      operationId: listZones
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Zones
          content:
            application/json:
              schema:
                type: object
                properties:
                  zones: { type: array, items: { $ref: '#/components/schemas/Zone' } }
    post:
      summary: Create a DNS zone
      operationId: createZone
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:       { type: string, example: example.com }
                zoneType:   { type: string, enum: [authoritative, reverse, forward] }
                forwarders: { type: array, items: { type: string } }
                primaryNs:  { type: string, nullable: true }
              required: [name, zoneType]
      responses:
        '201':
          description: Zone created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Zone' }

  /v1/dns/zones/export:
    get:
      summary: Export all zones as CSV
      operationId: exportZones
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: CSV file
          content:
            text/csv:
              schema:
                type: string

  /v1/dns/zones/import:
    post:
      summary: Import zones from CSV
      operationId: importZones
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          text/csv:
            schema:
              type: string
      responses:
        '200':
          description: Import result
          content:
            application/json:
              schema:
                type: object
                properties:
                  zonesCreated: { type: integer }
                  recsCreated:  { type: integer }

  /v1/dns/zones/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    delete:
      summary: Delete a zone
      operationId: deleteZone
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/dns/zones/{id}/site:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Assign a zone to a site
      operationId: assignZoneSite
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                siteId: { type: string, format: uuid, nullable: true }
              required: [siteId]
      responses:
        '200':
          description: Updated zone
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Zone' }

  /v1/dns/zones/{id}/acls:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Update zone ACLs and SOA parameters
      operationId: updateZoneACLs
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                allowQuery:   { type: string }
                allowTransfer: { type: string }
                soaRefresh:   { type: integer }
                soaRetry:     { type: integer }
                soaExpire:    { type: integer }
                soaMinimum:   { type: integer }
                soaRname:     { type: string }
              required: [allowQuery, allowTransfer]
      responses:
        '200':
          description: Updated zone
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Zone' }

  /v1/dns/zones/{id}/clone:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Clone a zone (copies all records)
      operationId: cloneZone
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, description: 'Name for the new zone' }
              required: [name]
      responses:
        '201':
          description: Cloned zone
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Zone' }

  /v1/dns/zones/{id}/records:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List records in a zone
      operationId: listRecords
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Records
          content:
            application/json:
              schema:
                type: object
                properties:
                  records: { type: array, items: { $ref: '#/components/schemas/DnsRecord' } }
    post:
      summary: Create a DNS record
      operationId: createRecord
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:  { type: string, example: www }
                rtype: { type: string, enum: [A, AAAA, CNAME, MX, TXT, NS, SRV, PTR, CAA, TLSA, SSHFP, NAPTR, CERT] }
                value: { type: string }
                ttl:   { type: integer, example: 3600 }
              required: [name, rtype, value, ttl]
      responses:
        '201':
          description: Record created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsRecord' }

  /v1/dns/zones/{id}/records/{recordId}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - name: recordId
        in: path
        required: true
        schema: { type: string, format: uuid }
    put:
      summary: Update a DNS record
      operationId: updateRecord
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:  { type: string }
                rtype: { type: string, enum: [A, AAAA, CNAME, MX, TXT, NS, SRV, PTR, CAA, TLSA, SSHFP, NAPTR, CERT] }
                value: { type: string }
                ttl:   { type: integer }
              required: [name, rtype, value, ttl]
      responses:
        '200':
          description: Updated record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsRecord' }
    delete:
      summary: Delete a DNS record
      operationId: deleteRecord
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/dns/zones/delete:
    post:
      summary: Delete multiple zones in one request
      operationId: deleteZones
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                zoneIds: { type: array, items: { type: string, format: uuid } }
              required: [zoneIds]
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  deletedZones:   { type: integer }
                  deletedRecords: { type: integer }

  /v1/dns/zones/{id}/records/delete:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Delete multiple records from a zone in one request
      operationId: deleteRecords
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                recordIds: { type: array, items: { type: string, format: uuid } }
              required: [recordIds]
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  deletedRecords: { type: integer }

  /v1/import/dns-zones:
    post:
      summary: Import DNS zones from a CSV upload
      description: |
        Columns: `zone_name,zone_type,comment`. Pass `?async=1` to run the import in
        the background and receive an ImportJob (poll `/v1/import/dns-jobs/{id}`); the
        `?duplicates=` mode controls how existing rows are handled.
      operationId: importDnsZoneRows
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - { name: async, in: query, schema: { type: string, enum: ['1'] } }
        - { name: duplicates, in: query, schema: { type: string, enum: [skip, update, error] } }
      requestBody:
        required: true
        content:
          text/csv:
            schema: { type: string }
      responses:
        '200':
          description: Synchronous import report
          content:
            application/json:
              schema: { type: object }
        '202':
          description: Async import started
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportJob' }

  /v1/import/dns-records:
    post:
      summary: Import DNS records from a CSV upload
      description: |
        Columns: `zone_name,record_name,rtype,value,ttl,comment`. Pass `?async=1` for a
        background import (poll `/v1/import/dns-jobs/{id}`).
      operationId: importDnsRecordRows
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - { name: async, in: query, schema: { type: string, enum: ['1'] } }
        - { name: duplicates, in: query, schema: { type: string, enum: [skip, update, error] } }
      requestBody:
        required: true
        content:
          text/csv:
            schema: { type: string }
      responses:
        '200':
          description: Synchronous import report
          content:
            application/json:
              schema: { type: object }
        '202':
          description: Async import started
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportJob' }

  /v1/import/dns-jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get the status of a DNS import job
      operationId: getDnsImportJob
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Import job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportJob' }
        '404':
          description: Not found

  /v1/dns/acls:
    get:
      summary: List named DNS ACLs
      operationId: listDnsACLs
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: ACLs
          content:
            application/json:
              schema:
                type: object
                properties:
                  acls: { type: array, items: { $ref: '#/components/schemas/DnsACL' } }
    post:
      summary: Create a named DNS ACL
      operationId: createDnsACL
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:        { type: string }
                entries:     { type: array, items: { type: string } }
                description: { type: string }
              required: [name, entries]
      responses:
        '201':
          description: ACL created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsACL' }

  /v1/dns/acls/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Update a named DNS ACL
      operationId: updateDnsACL
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:        { type: string }
                entries:     { type: array, items: { type: string } }
                description: { type: string }
              required: [name, entries]
      responses:
        '200':
          description: Updated ACL
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DnsACL' }
    delete:
      summary: Delete a named DNS ACL
      operationId: deleteDnsACL
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/dns/rpz:
    get:
      summary: List Response Policy Zones (DNS firewall)
      operationId: listRPZZones
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: RPZ zones
          content:
            application/json:
              schema:
                type: object
                properties:
                  zones: { type: array, items: { $ref: '#/components/schemas/RPZZone' } }
    post:
      summary: Create a Response Policy Zone
      operationId: createRPZZone
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:        { type: string }
                description: { type: string }
                priority:    { type: integer }
              required: [name]
      responses:
        '201':
          description: RPZ zone created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RPZZone' }

  /v1/dns/rpz/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    delete:
      summary: Delete a Response Policy Zone
      operationId: deleteRPZZone
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/dns/rpz/{id}/rules:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List rules in a Response Policy Zone
      operationId: listRPZRules
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: RPZ rules
          content:
            application/json:
              schema:
                type: object
                properties:
                  rules: { type: array, items: { $ref: '#/components/schemas/RPZRule' } }
    post:
      summary: Add a rule to a Response Policy Zone
      operationId: createRPZRule
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                trigger: { type: string, example: malware.example.com }
                action:  { type: string, description: 'NXDOMAIN, NODATA, PASSTHRU, DROP, or a CNAME rewrite target' }
                comment: { type: string }
              required: [trigger, action]
      responses:
        '201':
          description: RPZ rule created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RPZRule' }

  /v1/dns/rpz/{id}/rules/{ruleId}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: ruleId, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      summary: Delete a rule from a Response Policy Zone
      operationId: deleteRPZRule
      tags: [DNS]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  # ── IPAM ────────────────────────────────────────────────────────────────────

  /v1/ipam/subnets:
    get:
      summary: List subnets (full tree)
      operationId: listSubnets
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Subnets
          content:
            application/json:
              schema:
                type: object
                properties:
                  subnets: { type: array, items: { $ref: '#/components/schemas/IpamSubnet' } }
    post:
      summary: Create a subnet or container
      operationId: createSubnet
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:        { type: string }
                cidr:        { type: string, example: 10.10.1.0/24 }
                vlan:        { type: integer, nullable: true }
                gateway:     { type: string, nullable: true, example: 10.10.1.1 }
                vrf:         { type: string, nullable: true }
                parentId:    { type: string, format: uuid, nullable: true }
                isContainer: { type: boolean }
                attributes:  { type: object, additionalProperties: { type: string } }
              required: [name, cidr]
      responses:
        '201':
          description: Subnet created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamSubnet' }
        '409':
          description: CIDR overlaps an existing subnet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/ipam/subnets/merge:
    post:
      summary: Merge two adjacent sibling subnets into their parent
      operationId: mergeSubnets
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                subnetIds:   { type: array, items: { type: string, format: uuid }, minItems: 2, maxItems: 2 }
                asContainer: { type: boolean }
              required: [subnetIds]
      responses:
        '200':
          description: Merged subnet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamSubnet' }

  /v1/ipam/subnets/export:
    get:
      summary: Export all subnets as CSV
      operationId: exportSubnets
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: CSV download
          content:
            text/csv:
              schema: { type: string }

  /v1/ipam/subnets/import:
    post:
      summary: Import subnets from CSV
      operationId: importSubnets
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          text/csv:
            schema: { type: string }
            example: "name,cidr,vlan,gateway,vrf,is_container,parent_id,attributes\nEng VLAN,10.10.1.0/24,110,10.10.1.1,,false,,"
      responses:
        '200':
          description: Import result
          content:
            application/json:
              schema:
                type: object
                properties:
                  imported: { type: integer }

  /v1/ipam/subnets/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get a subnet
      operationId: getSubnet
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Subnet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamSubnet' }
        '404':
          description: Not found
    delete:
      summary: Delete a subnet
      operationId: deleteSubnet
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/ipam/subnets/{id}/ips:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List tracked IP records and DHCP pools in a subnet
      operationId: listSubnetIPs
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: IP records and DHCP pool overlays
          content:
            application/json:
              schema:
                type: object
                properties:
                  ips:   { type: array, items: { $ref: '#/components/schemas/IpamIPRecord' } }
                  pools: { type: array, items: { $ref: '#/components/schemas/DhcpPool' } }

  /v1/ipam/subnets/{id}/site:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Assign (or unassign) a subnet to a site
      operationId: assignSubnetSite
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                siteId:  { type: string, format: uuid, nullable: true }
                cascade: { type: boolean, description: 'If true and subnet is a container, propagate site to all children' }
              required: [siteId]
      responses:
        '200':
          description: Updated subnet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamSubnet' }

  /v1/ipam/subnets/{id}/options:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Update DNS/DHCP inheritable options for a subnet
      operationId: updateSubnetOptions
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                dnsOptions:  { $ref: '#/components/schemas/SubnetDnsOptions' }
                dhcpOptions: { $ref: '#/components/schemas/SubnetDhcpOptions' }
              required: [dnsOptions, dhcpOptions]
      responses:
        '200':
          description: Updated subnet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamSubnet' }

  /v1/ipam/subnets/{id}/attributes:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Replace custom attributes on a subnet
      operationId: updateSubnetAttributes
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                attributes: { type: object, additionalProperties: { type: string } }
              required: [attributes]
      responses:
        '200':
          description: Updated subnet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamSubnet' }

  /v1/ipam/subnets/{id}/split:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Split a subnet into equal child subnets
      operationId: splitSubnet
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prefixLen: { type: integer, description: 'New prefix length (must be > current)' }
              required: [prefixLen]
      responses:
        '200':
          description: Created child subnets (parent is now a container)
          content:
            application/json:
              schema:
                type: object
                properties:
                  subnets: { type: array, items: { $ref: '#/components/schemas/IpamSubnet' } }

  /v1/ipam/subnets/delete:
    post:
      summary: Delete multiple subnets in one request
      operationId: deleteSubnets
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                subnetIds:        { type: array, items: { type: string, format: uuid } }
                includeChildren:  { type: boolean }
                deleteDnsRecords: { type: boolean }
              required: [subnetIds]
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  deletedSubnets:    { type: integer }
                  deletedIpRecords:  { type: integer }
                  detachedScopes:    { type: integer }
                  deletedDnsRecords: { type: integer }

  /v1/ipam/subnets/{id}/allocate-ip:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Allocate the next free IP in a subnet
      operationId: allocateIP
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                hostname: { type: string }
                mac:      { type: string }
                status:   { type: string, enum: [static, reserved], description: 'Defaults to static' }
                comment:  { type: string }
      responses:
        '201':
          description: Allocated IP record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamIPRecord' }
        '409':
          description: Subnet is full
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/ipam/subnets/{id}/allocate-subnet:
    parameters:
      - $ref: '#/components/parameters/PathID'
    post:
      summary: Carve the next available child subnet from a container
      operationId: allocateSubnet
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:      { type: string }
                prefixLen: { type: integer, description: 'Prefix length of the new child subnet' }
                comment:   { type: string }
              required: [prefixLen]
      responses:
        '201':
          description: Allocated child subnet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamSubnet' }
        '409':
          description: No free block of the requested size
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/ipam/subnets/{id}/ips/{ip}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: ip, in: path, required: true, schema: { type: string } }
    get:
      summary: Get a single tracked IP record
      operationId: getSubnetIP
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: IP record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamIPRecord' }
        '404':
          description: Not found

  /v1/ipam/subnets/{id}/ips/{ip}/status:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: ip, in: path, required: true, schema: { type: string } }
    put:
      summary: Set the lifecycle status of an IP
      operationId: setIPStatus
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status: { type: string, enum: [static, reserved, reserved_dhcp, allocated, discovered] }
              required: [status]
      responses:
        '200':
          description: Updated IP record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamIPRecord' }

  /v1/ipam/subnets/{id}/ips/{ip}/comment:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: ip, in: path, required: true, schema: { type: string } }
    put:
      summary: Set the comment on an IP
      operationId: updateIPComment
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                comment: { type: string }
              required: [comment]
      responses:
        '200':
          description: Updated IP record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamIPRecord' }

  /v1/ipam/subnets/{id}/ips/{ip}/attributes:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - { name: ip, in: path, required: true, schema: { type: string } }
    put:
      summary: Replace custom attributes on an IP
      operationId: updateIPAttributes
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                attributes: { type: object, additionalProperties: { type: string } }
              required: [attributes]
      responses:
        '200':
          description: Updated IP record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpamIPRecord' }

  /v1/import/ip-subnets:
    post:
      summary: Import subnets from a CSV upload
      description: |
        Columns: `name,cidr,vlan,gateway,vrf,is_container,parent_id,attributes`. Pass
        `?async=1` for a background import (poll `/v1/import/ipam-jobs/{id}`).
      operationId: importIpSubnetRows
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - { name: async, in: query, schema: { type: string, enum: ['1'] } }
      requestBody:
        required: true
        content:
          text/csv:
            schema: { type: string }
      responses:
        '200':
          description: Synchronous import report
          content:
            application/json:
              schema: { type: object }
        '202':
          description: Async import started
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportJob' }

  /v1/import/ip-records:
    post:
      summary: Import IP records from a CSV upload
      description: |
        Columns: `subnet_cidr,ip,status,hostname,mac,comment`. Pass `?async=1` for a
        background import (poll `/v1/import/ipam-jobs/{id}`).
      operationId: importIpRecordRows
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - { name: async, in: query, schema: { type: string, enum: ['1'] } }
      requestBody:
        required: true
        content:
          text/csv:
            schema: { type: string }
      responses:
        '200':
          description: Synchronous import report
          content:
            application/json:
              schema: { type: object }
        '202':
          description: Async import started
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportJob' }

  /v1/import/ipam-jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get the status of an IPAM import job
      operationId: getIpamImportJob
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Import job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportJob' }
        '404':
          description: Not found

  /v1/import/jobs:
    get:
      summary: List recent import jobs for the tenant (DNS and IPAM)
      operationId: listImportJobs
      tags: [IPAM]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - { name: limit, in: query, schema: { type: integer } }
      responses:
        '200':
          description: Import jobs feed
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: '#/components/schemas/ImportJob' } }

  # ── DHCP ────────────────────────────────────────────────────────────────────

  /v1/dhcp/search:
    get:
      summary: Search leases and reservations across DHCP scopes
      operationId: searchLeases
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2 }
          description: Search by IP, MAC, or hostname.
      responses:
        '200':
          description: Matching leases and reservations
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { $ref: '#/components/schemas/LeaseSearchResult' } }
                required: [results]
        '400':
          description: Query is missing or too short
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/dhcp/scopes:
    get:
      summary: List DHCP scopes
      operationId: listScopes
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Scopes
          content:
            application/json:
              schema:
                type: object
                properties:
                  scopes: { type: array, items: { $ref: '#/components/schemas/DhcpScope' } }
    post:
      summary: Create a DHCP scope
      operationId: createScope
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:       { type: string }
                cidr:       { type: string, example: 10.10.1.0/24 }
                rangeStart: { type: string, example: 10.10.1.10 }
                rangeEnd:   { type: string, example: 10.10.1.250 }
                subnetId:   { type: string, format: uuid, nullable: true }
                options:    { type: array, items: { $ref: '#/components/schemas/ScopeOption' } }
              required: [name, cidr, rangeStart, rangeEnd]
      responses:
        '201':
          description: Scope created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpScope' }

  /v1/dhcp/scopes/{id}:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Update scope name, range, or DDNS settings
      operationId: updateScope
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:                    { type: string }
                rangeStart:              { type: string }
                rangeEnd:                { type: string }
                ddnsQualifyingSuffix:    { type: string }
                ddnsOverrideClientUpdate: { type: boolean }
                sharedNetwork:           { type: string }
              required: [name, rangeStart, rangeEnd]
      responses:
        '200':
          description: Updated scope
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpScope' }
    delete:
      summary: Delete a scope
      operationId: deleteScope
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/dhcp/scopes/{id}/site:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Assign scope to a site
      operationId: assignScopeSite
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                siteId: { type: string, format: uuid, nullable: true }
              required: [siteId]
      responses:
        '200':
          description: Updated scope
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpScope' }

  /v1/dhcp/scopes/{id}/agent:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Assign scope to a specific agent (or unassign)
      description: |
        A DHCP scope must be served by exactly one agent. Assigning to multiple agents
        causes duplicate IP offers. Set agentId to null to unassign (scope won't be deployed).
      operationId: assignScopeAgent
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                agentId: { type: string, format: uuid, nullable: true }
              required: [agentId]
      responses:
        '200':
          description: Updated scope
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpScope' }

  /v1/dhcp/scopes/{id}/enabled:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Enable or disable a DHCP scope
      operationId: toggleScopeEnabled
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled: { type: boolean }
              required: [enabled]
      responses:
        '200':
          description: Updated scope
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpScope' }

  /v1/dhcp/scopes/{id}/options:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get DHCP options for a scope
      operationId: getScopeOptions
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Options
          content:
            application/json:
              schema:
                type: object
                properties:
                  options: { type: array, items: { $ref: '#/components/schemas/ScopeOption' } }
    put:
      summary: Replace DHCP options for a scope
      operationId: setScopeOptions
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                options: { type: array, items: { $ref: '#/components/schemas/ScopeOption' } }
              required: [options]
      responses:
        '200':
          description: Updated options
          content:
            application/json:
              schema:
                type: object
                properties:
                  options: { type: array, items: { $ref: '#/components/schemas/ScopeOption' } }

  /v1/dhcp/scopes/{id}/leases:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: Get active leases for a scope
      operationId: listLeases
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Active leases
          content:
            application/json:
              schema:
                type: object
                properties:
                  leases: { type: array, items: { $ref: '#/components/schemas/DhcpLease' } }

  /v1/dhcp/scopes/{id}/leases/{ip}/reserve:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - name: ip
        in: path
        required: true
        schema: { type: string }
    post:
      summary: Create a reservation from an active lease
      operationId: reserveLease
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '201':
          description: Reservation created from lease
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpReservation' }
        '404':
          description: Lease not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: MAC address already has a reservation in this tenant
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/dhcp/scopes/{id}/reservations:
    parameters:
      - $ref: '#/components/parameters/PathID'
    get:
      summary: List reservations in a scope
      operationId: listReservations
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Reservations
          content:
            application/json:
              schema:
                type: object
                properties:
                  reservations: { type: array, items: { $ref: '#/components/schemas/DhcpReservation' } }
    post:
      summary: Create a reservation
      operationId: createReservation
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mac:        { type: string, example: 'AA:BB:CC:DD:EE:FF' }
                ip:         { type: string, example: 10.10.1.50 }
                hostname:   { type: string }
                clientId:   { type: string }
                bootFile:   { type: string }
                nextServer: { type: string }
              required: [mac, ip]
      responses:
        '201':
          description: Reservation created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpReservation' }
        '409':
          description: MAC address already has a reservation in this tenant
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/dhcp/scopes/{id}/reservations/{resId}:
    parameters:
      - $ref: '#/components/parameters/PathID'
      - name: resId
        in: path
        required: true
        schema: { type: string, format: uuid }
    put:
      summary: Update a reservation
      operationId: updateReservation
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mac:        { type: string }
                ip:         { type: string }
                hostname:   { type: string }
                clientId:   { type: string }
                bootFile:   { type: string }
                nextServer: { type: string }
              required: [mac, ip]
      responses:
        '200':
          description: Updated reservation
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpReservation' }
    delete:
      summary: Delete a reservation
      operationId: deleteReservation
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/dhcp/scopes/{id}/ha-group:
    parameters:
      - $ref: '#/components/parameters/PathID'
    put:
      summary: Assign a scope to a DHCP high-availability group (or unassign)
      operationId: assignScopeHAGroup
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                haGroupId: { type: string, format: uuid, nullable: true }
              required: [haGroupId]
      responses:
        '200':
          description: Updated scope
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DhcpScope' }

  /v1/dhcp/ha-groups:
    get:
      summary: List DHCP high-availability groups
      operationId: listHAGroups
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: HA groups
          content:
            application/json:
              schema:
                type: object
                properties:
                  groups: { type: array, items: { $ref: '#/components/schemas/HAGroup' } }
    post:
      summary: Create a DHCP HA group
      operationId: createHAGroup
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:               { type: string }
                mode:               { type: string, enum: [hot-standby, load-balancing, passive-backup] }
                heartbeatDelayMs:   { type: integer }
                maxResponseDelayMs: { type: integer }
                maxUnackedClients:  { type: integer }
                maxAckDelayMs:      { type: integer }
              required: [name, mode]
      responses:
        '201':
          description: HA group created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HAGroup' }

  /v1/dhcp/ha-groups/{groupId}:
    parameters:
      - { name: groupId, in: path, required: true, schema: { type: string, format: uuid } }
    put:
      summary: Update a DHCP HA group
      operationId: updateHAGroup
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:               { type: string }
                mode:               { type: string, enum: [hot-standby, load-balancing, passive-backup] }
                heartbeatDelayMs:   { type: integer }
                maxResponseDelayMs: { type: integer }
                maxUnackedClients:  { type: integer }
                maxAckDelayMs:      { type: integer }
      responses:
        '200':
          description: Updated HA group
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HAGroup' }
    delete:
      summary: Delete a DHCP HA group
      operationId: deleteHAGroup
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

  /v1/dhcp/ha-groups/{groupId}/members:
    parameters:
      - { name: groupId, in: path, required: true, schema: { type: string, format: uuid } }
    put:
      summary: Add or update a member (agent) of a DHCP HA group
      operationId: upsertHAGroupMember
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                agentId: { type: string, format: uuid }
                role:    { type: string, enum: [primary, standby, backup] }
                peerUrl: { type: string, example: 'http://10.0.0.5:8000/' }
              required: [agentId, role]
      responses:
        '200':
          description: Updated HA group
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HAGroup' }

  /v1/dhcp/ha-groups/{groupId}/members/{agentId}:
    parameters:
      - { name: groupId, in: path, required: true, schema: { type: string, format: uuid } }
      - { name: agentId, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      summary: Remove a member (agent) from a DHCP HA group
      operationId: deleteHAGroupMember
      tags: [DHCP]
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '204': { description: Deleted }

tags:
  - name: Observability
    description: Health check and Prometheus metrics
  - name: Auth
    description: Current user identity, profile, and tenant memberships
  - name: Account
    description: Self-service account deletion and recovery
  - name: API Tokens
    description: Long-lived bearer tokens for programmatic access
  - name: Settings
    description: Tenant settings and security policy
  - name: Audit
    description: Tenant audit log, export, and revert
  - name: Superadmin
    description: Platform-level tenant and user management (superuser only)
  - name: Admin
    description: Tenant-admin member management and data export
  - name: MSP
    description: Managed-service-provider client management and reporting
  - name: Webhooks
    description: Outbound event webhooks and delivery history
  - name: Notifications
    description: In-app and email/push notifications and preferences
  - name: Sites
    description: Geographic or logical groupings of agents
  - name: Agents
    description: DNS/DHCP agent management and enrollment
  - name: DNS
    description: DNS zone and record management
  - name: IPAM
    description: IP address management — subnets, containers, and IP tracking
  - name: DHCP
    description: DHCP scope, option, reservation, and lease management
