openapi: 3.1.0
info:
  title: Eidonix Lens API
  version: 1.0.0
  description: Self-hosted general-purpose information discovery, ingestion, search, and retrieval.
servers:
  - url: http://127.0.0.1:8080
security:
  - bearerAuth: []
tags:
  - name: Health
  - name: Search
  - name: Documents
  - name: Collections
  - name: Administration
paths:
  /:
    get:
      tags: [Health]
      operationId: landingPage
      security: []
      responses:
        "200":
          description: Dependency-free browser search and document-upload console.
          content:
            text/html:
              schema: {type: string}
  /openapi.yaml:
    get:
      tags: [Health]
      operationId: openAPISpecification
      security: []
      responses:
        "200":
          description: This OpenAPI 3.1 contract.
          content:
            application/yaml:
              schema: {type: string}
  /healthz/live:
    get:
      tags: [Health]
      operationId: live
      security: []
      responses:
        "200":
          description: Process is alive.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, const: ok}
  /healthz/ready:
    get:
      tags: [Health]
      operationId: ready
      security: []
      responses:
        "200":
          description: All required durable dependencies are ready.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Health"}
        "503": {$ref: "#/components/responses/Error"}
  /v1/search:
    post:
      tags: [Search]
      operationId: search
      description: Search retained evidence and, when justified by the selected depth, perform bounded live discovery.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/SearchRequest"}
      responses:
        "200":
          description: Ranked evidence and optional cited answer.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/SearchResponse"}
        "400": {$ref: "#/components/responses/Error"}
        "401": {$ref: "#/components/responses/Error"}
        "403": {$ref: "#/components/responses/Error"}
        "429": {$ref: "#/components/responses/Error"}
  /v1/extract:
    post:
      tags: [Search]
      operationId: extractURLs
      description: Safely fetch and extract up to ten public URLs, optionally retaining them.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [urls, index]
              properties:
                urls:
                  type: array
                  minItems: 1
                  maxItems: 10
                  items: {type: string, format: uri}
                collection: {type: string, default: default}
                index: {type: boolean}
      responses:
        "200":
          description: Per-URL extraction results; individual URL failures appear in each result.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/ExtractResponse"}
        "400": {$ref: "#/components/responses/Error"}
        "401": {$ref: "#/components/responses/Error"}
        "403": {$ref: "#/components/responses/Error"}
  /v1/documents:
    get:
      tags: [Documents]
      operationId: listDocuments
      parameters:
        - in: query
          name: limit
          schema: {type: integer, minimum: 1, maximum: 500, default: 100}
        - in: query
          name: offset
          schema: {type: integer, minimum: 0, default: 0}
        - in: query
          name: collection
          schema: {type: string}
        - in: query
          name: source_type
          schema: {type: string, enum: [catalog, internet]}
        - in: query
          name: status
          schema: {type: string, enum: [queued, processing, ready, failed, deleted]}
        - in: query
          name: crawler_target_id
          schema: {type: string}
        - in: query
          name: q
          schema: {type: string}
      responses:
        "200":
          description: Most recently created documents.
          content:
            application/json:
              schema:
                type: object
                required: [documents]
                properties:
                  documents:
                    type: array
                    items: {$ref: "#/components/schemas/Document"}
                  total: {type: integer}
                  limit: {type: integer}
                  offset: {type: integer}
        "401": {$ref: "#/components/responses/Error"}
        "403": {$ref: "#/components/responses/Error"}
    post:
      tags: [Documents]
      operationId: ingestDocument
      description: Durably stage one uploaded text-bearing file, PDF, or OCR-backed document image and queue background extraction, embedding, and indexing.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
                title: {type: string}
                source_uri: {type: string}
                collection: {type: string, default: default}
                language: {type: string}
                published_at: {type: string, format: date-time}
                metadata:
                  type: string
                  description: JSON object encoded as one multipart string field.
      responses:
        "202":
          description: Upload staged and ingestion job queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/QueuedJob"}
        "400": {$ref: "#/components/responses/Error"}
        "401": {$ref: "#/components/responses/Error"}
        "403": {$ref: "#/components/responses/Error"}
        "422": {$ref: "#/components/responses/Error"}
  /v1/documents/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    get:
      tags: [Documents]
      operationId: getDocument
      responses:
        "200":
          description: Document metadata and complete normalized text.
          content:
            application/json:
              schema:
                type: object
                required: [document, content]
                properties:
                  document: {$ref: "#/components/schemas/Document"}
                  content: {type: string}
        "404": {$ref: "#/components/responses/Error"}
    delete:
      tags: [Documents]
      operationId: deleteDocument
      description: Remove serving-index points and mark the tenant document deleted.
      responses:
        "204": {description: Deleted.}
        "404": {$ref: "#/components/responses/Error"}
  /v1/documents/{id}/raw:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    get:
      tags: [Documents]
      operationId: downloadDocument
      description: Download the retained original bytes for a catalog or Internet item.
      responses:
        "200":
          description: Original document with its retained media type and attachment filename.
          content:
            application/octet-stream:
              schema: {type: string, format: binary}
        "404": {$ref: "#/components/responses/Error"}
  /v1/datasets:
    post:
      tags: [Documents]
      operationId: ingestDataset
      description: Durably stage a JSONL dataset or bounded ZIP archive and queue background ingestion.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: {type: string, format: binary}
                collection: {type: string, default: default}
      responses:
        "202":
          description: Dataset staged and ingestion job queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/QueuedJob"}
        "422": {$ref: "#/components/responses/Error"}
  /v1/jobs/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    get:
      tags: [Documents]
      operationId: getIngestionJob
      description: Poll an ingestion job submitted by this API key; administrators may read any tenant job.
      responses:
        "200":
          description: Current job state, progress, and final result when complete.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Job"}
        "404": {$ref: "#/components/responses/Error"}
  /v1/usage:
    get:
      tags: [Administration]
      operationId: getUsage
      responses:
        "200":
          description: Aggregate work-based usage for the authenticated tenant.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Usage"}
  /v1/collections:
    get:
      tags: [Collections]
      operationId: listCollections
      responses:
        "200":
          description: Collections and document counts.
          content:
            application/json:
              schema:
                type: object
                required: [collections]
                properties:
                  collections:
                    type: array
                    items: {$ref: "#/components/schemas/Collection"}
    post:
      tags: [Collections]
      operationId: upsertCollection
      description: Administrator-only generic collection creation/update.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug]
              properties:
                slug: {type: string, minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9][A-Za-z0-9_-]*$"}
                name: {type: string, maxLength: 200}
                description: {type: string, maxLength: 4000}
                config: {type: object, additionalProperties: true}
      responses:
        "200":
          description: Created or updated collection.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Collection"}
        "400": {$ref: "#/components/responses/Error"}
        "403": {$ref: "#/components/responses/Error"}
  /v1/collections/{slug}:
    parameters:
      - in: path
        name: slug
        required: true
        schema: {type: string}
    delete:
      tags: [Collections]
      operationId: deleteEmptyCollection
      description: Delete a non-default collection after its documents and crawler targets have been removed.
      responses:
        "204": {description: Collection deleted.}
        "400": {$ref: "#/components/responses/Error"}
        "409": {$ref: "#/components/responses/Error"}
  /v1/admin/api-keys:
    get:
      tags: [Administration]
      operationId: listAPIKeys
      responses:
        "200":
          description: Tenant keys; hashes and secrets are never returned.
          content:
            application/json:
              schema:
                type: object
                required: [api_keys]
                properties:
                  api_keys:
                    type: array
                    items: {$ref: "#/components/schemas/APIKey"}
        "403": {$ref: "#/components/responses/Error"}
    post:
      tags: [Administration]
      operationId: createAPIKey
      description: Create a tenant key. The secret appears in this response only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string, minLength: 1, maxLength: 100}
                is_admin: {type: boolean, default: false}
                capabilities:
                  type: array
                  uniqueItems: true
                  items: {type: string, enum: [search, extract, ingest]}
      responses:
        "201":
          description: Key metadata and one-time plaintext secret.
          content:
            application/json:
              schema:
                type: object
                required: [api_key, secret]
                properties:
                  api_key: {$ref: "#/components/schemas/APIKey"}
                  secret: {type: string}
        "400": {$ref: "#/components/responses/Error"}
  /v1/admin/api-keys/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    delete:
      tags: [Administration]
      operationId: revokeAPIKey
      description: Revoke another active tenant key; a key cannot revoke itself.
      responses:
        "204": {description: Revoked.}
        "400": {$ref: "#/components/responses/Error"}
        "404": {$ref: "#/components/responses/Error"}
  /v1/admin/domain-policies:
    get:
      tags: [Administration]
      operationId: listDomainPolicies
      responses:
        "200":
          description: Tenant-wide blocked domains.
          content:
            application/json:
              schema:
                type: object
                required: [domain_policies]
                properties:
                  domain_policies:
                    type: array
                    items: {$ref: "#/components/schemas/DomainPolicy"}
    post:
      tags: [Administration]
      operationId: upsertDomainPolicy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: {type: string}
                reason: {type: string, maxLength: 1000}
      responses:
        "200":
          description: Domain exclusion created or updated.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/DomainPolicy"}
        "400": {$ref: "#/components/responses/Error"}
  /v1/admin/domain-policies/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    delete:
      tags: [Administration]
      operationId: deleteDomainPolicy
      responses:
        "204": {description: Domain unblocked.}
        "404": {$ref: "#/components/responses/Error"}
  /v1/admin/stats:
    get:
      tags: [Administration]
      operationId: getIndexStats
      responses:
        "200":
          description: Tenant control-plane and serving-index counts.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/IndexStats"}
  /v1/admin/audit:
    get:
      tags: [Administration]
      operationId: listAuditEvents
      parameters:
        - in: query
          name: limit
          schema: {type: integer, minimum: 1, maximum: 1000, default: 100}
      responses:
        "200":
          description: Most recent tenant administrative audit events.
          content:
            application/json:
              schema:
                type: object
                required: [audit_events]
                properties:
                  audit_events:
                    type: array
                    items: {$ref: "#/components/schemas/AuditEvent"}
  /v1/admin/seeds:
    get:
      tags: [Administration]
      operationId: listSeeds
      responses:
        "200":
          description: Crawl seeds and schedules.
          content:
            application/json:
              schema:
                type: object
                required: [seeds]
                properties:
                  seeds:
                    type: array
                    items: {$ref: "#/components/schemas/Seed"}
    post:
      tags: [Administration]
      operationId: addSeed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: {type: string, format: uri}
                collection: {type: string, default: default}
      responses:
        "202":
          description: Seed saved and initial crawl queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Seed"}
        "400": {$ref: "#/components/responses/Error"}
  /v1/admin/crawler-targets:
    get:
      tags: [Administration]
      operationId: listCrawlerTargets
      responses:
        "200":
          description: Managed Internet exploration targets.
          content:
            application/json:
              schema:
                type: object
                required: [seeds]
                properties:
                  seeds:
                    type: array
                    items: {$ref: "#/components/schemas/Seed"}
    post:
      tags: [Administration]
      operationId: createCrawlerTarget
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CrawlerTargetInput"}
      responses:
        "202":
          description: Target saved and initial crawl queued.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Seed"}
        "400": {$ref: "#/components/responses/Error"}
  /v1/admin/crawler-targets/{id}:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    put:
      tags: [Administration]
      operationId: updateCrawlerTarget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/CrawlerTargetSettings"
                - type: object
                  required: [name, enabled, recrawl_interval_seconds, max_pages, max_depth]
      responses:
        "200":
          description: Updated target.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Seed"}
        "404": {$ref: "#/components/responses/Error"}
    delete:
      tags: [Administration]
      operationId: deleteCrawlerTarget
      description: Delete the target and queued work while retaining already indexed results.
      responses:
        "204": {description: Target deleted.}
        "404": {$ref: "#/components/responses/Error"}
  /v1/admin/crawler-targets/{id}/run:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    post:
      tags: [Administration]
      operationId: runCrawlerTarget
      responses:
        "202":
          description: Crawl queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, const: queued}
                  job_id: {type: string}
        "404": {$ref: "#/components/responses/Error"}
  /v1/admin/crawler-targets/{id}/sites:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    get:
      tags: [Administration]
      operationId: listProbeSites
      responses:
        "200":
          description: Sites autonomously discovered and tracked by this probe.
          content:
            application/json:
              schema:
                type: object
                required: [sites]
                properties:
                  sites:
                    type: array
                    items: {$ref: "#/components/schemas/ProbeSite"}
        "404": {$ref: "#/components/responses/Error"}
  /v1/admin/jobs:
    get:
      tags: [Administration]
      operationId: listJobs
      parameters:
        - in: query
          name: limit
          schema: {type: integer, minimum: 1, maximum: 500, default: 100}
      responses:
        "200":
          description: Most recent tenant jobs.
          content:
            application/json:
              schema:
                type: object
                required: [jobs]
                properties:
                  jobs:
                    type: array
                    items: {$ref: "#/components/schemas/Job"}
  /v1/admin/jobs/{id}/retry:
    parameters:
      - $ref: "#/components/parameters/ResourceID"
    post:
      tags: [Administration]
      operationId: retryJob
      responses:
        "202":
          description: Failed job returned to the queue.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, const: queued}
        "404": {$ref: "#/components/responses/Error"}
  /v1/admin/reindex:
    post:
      tags: [Administration]
      operationId: reindex
      description: Queue replay of every ready tenant document from S3 into the current Qdrant generation.
      responses:
        "202":
          description: Reindex jobs queued.
          content:
            application/json:
              schema:
                type: object
                required: [accepted]
                properties:
                  accepted: {type: integer, minimum: 0}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  parameters:
    ResourceID:
      in: path
      name: id
      required: true
      schema: {type: string}
  responses:
    Error:
      description: Structured error.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: {type: string}
            message: {type: string}
    Health:
      type: object
      required: [status, components]
      properties:
        status: {type: string, enum: [ok, degraded]}
        components:
          type: object
          additionalProperties: {type: string}
    SearchRequest:
      type: object
      required: [query]
      additionalProperties: false
      properties:
        query: {type: string, minLength: 1, maxLength: 4000}
        search_depth: {type: string, enum: [basic, advanced], default: basic}
        max_results: {type: integer, minimum: 1, maximum: 50, default: 8}
        include_answer: {type: boolean, default: false}
        include_domains:
          type: array
          items: {type: string}
        exclude_domains:
          type: array
          items: {type: string}
        time_range: {type: string, enum: [day, week, month, year]}
        collections:
          type: array
          items: {type: string}
        media_types:
          type: array
          items: {type: string}
        source_types:
          type: array
          items: {type: string, enum: [catalog, internet]}
    SearchResponse:
      type: object
      required: [request_id, query, results, usage]
      properties:
        request_id: {type: string}
        query: {type: string}
        answer: {type: string}
        results:
          type: array
          items: {$ref: "#/components/schemas/SearchResult"}
        usage: {$ref: "#/components/schemas/SearchUsage"}
    SearchResult:
      type: object
      required: [id, document_id, segment_id, title, content, score, retrieval_score, rerank_score]
      properties:
        id: {type: string}
        document_id: {type: string}
        segment_id: {type: string}
        title: {type: string}
        url: {type: string}
        source_uri: {type: string}
        source_type: {type: string, enum: [catalog, internet]}
        document_url: {type: string, format: uri}
        download_url: {type: string, format: uri}
        content: {type: string}
        score: {type: number, minimum: 0, maximum: 1}
        retrieval_score:
          type: number
          description: Dense and lexical retrieval-fusion score before reranking.
        rerank_score:
          type: number
          description: Score from the configured reranking stage (external model when enabled, otherwise deterministic exact-term reranking) before final list normalization.
        media_type: {type: string}
        language: {type: string}
        published_at: {type: string, format: date-time}
        fetched_at: {type: string, format: date-time}
        metadata: {type: object, additionalProperties: true}
    SearchUsage:
      type: object
      required: [searches, candidates_retrieved, documents_reranked, pages_fetched]
      properties:
        searches: {type: integer}
        candidates_retrieved: {type: integer}
        documents_reranked: {type: integer}
        pages_fetched: {type: integer}
        embedding_tokens: {type: integer}
        synthesis_input_tokens: {type: integer}
    ExtractResponse:
      type: object
      required: [request_id, results]
      properties:
        request_id: {type: string}
        results:
          type: array
          items:
            type: object
            required: [url]
            properties:
              url: {type: string}
              title: {type: string}
              media_type: {type: string}
              content: {type: string}
              document_id: {type: string}
              metadata: {type: object, additionalProperties: true}
              error: {type: string}
    Document:
      type: object
      required: [id, collection, title, media_type, content_hash, status, created_at, updated_at]
      properties:
        id: {type: string}
        collection: {type: string}
        source_uri: {type: string}
        source_type: {type: string, enum: [catalog, internet]}
        crawler_target_id: {type: string}
        title: {type: string}
        media_type: {type: string}
        language: {type: string}
        content_hash: {type: string}
        raw_object_key: {type: string}
        text_object_key: {type: string}
        metadata: {type: object, additionalProperties: true}
        status: {type: string, enum: [queued, processing, ready, failed, deleted]}
        error: {type: string}
        published_at: {type: string, format: date-time}
        fetched_at: {type: string, format: date-time}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
        processor: {type: string}
        processor_version: {type: string}
        segmenter_version: {type: string}
        embedding_model: {type: string}
    DatasetResponse:
      type: object
      required: [dataset_id, accepted, rejected, document_ids]
      properties:
        dataset_id: {type: string}
        accepted: {type: integer, minimum: 0}
        rejected: {type: integer, minimum: 0}
        document_ids:
          type: array
          items: {type: string}
        errors:
          type: array
          items: {type: string}
    QueuedJob:
      type: object
      required: [status, job_id]
      properties:
        status: {type: string, const: queued}
        job_id: {type: string}
    Usage:
      type: object
      required: [tenant_id, searches, documents_ingested, pages_fetched, bytes_stored, embedding_calls, synthesis_calls]
      properties:
        tenant_id: {type: string}
        searches: {type: integer}
        documents_ingested: {type: integer}
        pages_fetched: {type: integer}
        bytes_stored: {type: integer}
        embedding_calls: {type: integer}
        synthesis_calls: {type: integer}
    Collection:
      type: object
      required: [slug, name, config, document_count, ready_documents, created_at, updated_at]
      properties:
        slug: {type: string}
        name: {type: string}
        description: {type: string}
        config: {type: object, additionalProperties: true}
        document_count: {type: integer}
        ready_documents: {type: integer}
        catalog_items: {type: integer}
        internet_items: {type: integer}
        failed_items: {type: integer}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    APIKey:
      type: object
      required: [id, name, is_admin, capabilities, created_at]
      properties:
        id: {type: string}
        name: {type: string}
        is_admin: {type: boolean}
        capabilities:
          type: array
          items: {type: string}
        created_at: {type: string, format: date-time}
        revoked_at: {type: string, format: date-time}
    DomainPolicy:
      type: object
      required: [id, domain, created_at, updated_at]
      properties:
        id: {type: string}
        domain: {type: string}
        reason: {type: string}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    Seed:
      type: object
      required: [id, name, collection, intent, enabled, target_site_count, discovered_sites, crawled_sites, created_at]
      properties:
        id: {type: string}
        name: {type: string}
        collection: {type: string}
        intent: {type: string, minLength: 3, maxLength: 4000}
        url:
          type: string
          format: uri
          description: Optional operator-supplied starting URL; discovery is not restricted to it.
        enabled: {type: boolean}
        recrawl_interval_seconds: {type: integer, minimum: 300, maximum: 31536000}
        max_pages: {type: integer, minimum: 1, maximum: 10000}
        max_depth: {type: integer, minimum: 0, maximum: 10}
        target_site_count: {type: integer, minimum: 1, maximum: 500}
        discovered_sites: {type: integer, minimum: 0}
        crawled_sites: {type: integer, minimum: 0}
        last_error: {type: string}
        created_at: {type: string, format: date-time}
        last_run_at: {type: string, format: date-time}
        next_run_at: {type: string, format: date-time}
    CrawlerTargetSettings:
      type: object
      properties:
        name: {type: string, minLength: 1, maxLength: 200}
        intent: {type: string, minLength: 3, maxLength: 4000}
        enabled: {type: boolean}
        recrawl_interval_seconds: {type: integer, minimum: 300, maximum: 31536000}
        max_pages: {type: integer, minimum: 1, maximum: 10000}
        max_depth: {type: integer, minimum: 0, maximum: 10}
        target_site_count: {type: integer, minimum: 1, maximum: 500, default: 50}
    CrawlerTargetInput:
      allOf:
        - $ref: "#/components/schemas/CrawlerTargetSettings"
        - type: object
          required: [intent, collection]
          properties:
            url: {type: string, format: uri}
            collection: {type: string}
            start_immediately: {type: boolean, default: true}
    ProbeSite:
      type: object
      required: [id, probe_id, site_key, hostname, root_url, provider, discovery_score, enabled, state, pages_indexed, first_discovered_at, last_discovered_at]
      properties:
        id: {type: string}
        probe_id: {type: string}
        site_key: {type: string}
        hostname: {type: string}
        root_url: {type: string, format: uri}
        title: {type: string}
        snippet: {type: string}
        provider: {type: string, description: Connector that admitted the site, such as searxng, brave, tavily, or configured.}
        source_query: {type: string}
        discovery_score: {type: number, minimum: 0, maximum: 1}
        enabled: {type: boolean}
        state: {type: string, enum: [discovered, crawled, failed, paused]}
        pages_indexed: {type: integer, minimum: 0}
        first_discovered_at: {type: string, format: date-time}
        last_discovered_at: {type: string, format: date-time}
        last_crawled_at: {type: string, format: date-time}
        last_error: {type: string}
    Job:
      type: object
      required: [id, kind, status, attempts, max_attempts, available_at, created_at, updated_at]
      properties:
        id: {type: string}
        kind: {type: string}
        status: {type: string, enum: [queued, running, succeeded, failed]}
        attempts: {type: integer}
        max_attempts: {type: integer}
        last_error: {type: string}
        payload: {type: object, additionalProperties: true}
        result: {type: object, additionalProperties: true}
        progress_current: {type: integer, minimum: 0}
        progress_total: {type: integer, minimum: 0}
        progress_message: {type: string}
        available_at: {type: string, format: date-time}
        leased_until: {type: string, format: date-time}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    IndexStats:
      type: object
      required: [documents_by_status, jobs_by_status, collections, seeds, vector_points]
      properties:
        documents_by_status:
          type: object
          additionalProperties: {type: integer}
        jobs_by_status:
          type: object
          additionalProperties: {type: integer}
        collections: {type: integer}
        seeds: {type: integer}
        vector_points: {type: integer}
    AuditEvent:
      type: object
      required: [id, action, target_type, detail, created_at]
      properties:
        id: {type: integer}
        key_id: {type: string}
        action: {type: string}
        target_type: {type: string}
        target_id: {type: string}
        detail: {type: object, additionalProperties: true}
        created_at: {type: string, format: date-time}
