diff --git a/.env.example b/.env.example index 68a80ec71..7da2e6b61 100644 --- a/.env.example +++ b/.env.example @@ -232,7 +232,8 @@ DEVFEED_FARO_ENABLED=false DEVFEED_AI_QUOTA_PACING_ENABLED=false DEVFEED_AI_QUOTA_RESERVE_PERCENT=20 -# Image storage: credentials stay in the worker and user API; reader APIs use the public URL. +# Image storage: credentials stay in the images worker and private APIs for avatar/ad uploads. +# Public reader APIs and frontends use only the public URL. # Enable the `images` Compose profile for imgproxy and the dedicated Images worker. # Topic logos require public GET/HEAD CORS for card exports; see apps/aggregator/TOPIC_LOGOS.md. DEVFEED_IMAGE_STORAGE_ENABLED=false @@ -270,3 +271,35 @@ DEVFEED_ARTICLE_TOPIC_PROPOSALS_ENABLED=true # DEVFEED_MCP_ACCESS_TOKEN_TTL_SECONDS=900 # DEVFEED_USER_MCP_SESSION_TTL_SECONDS=2592000 # DEVFEED_USER_MCP_SESSION_ABSOLUTE_TTL_SECONDS=7776000 + +# Partner portal: use a separate Zitadel application/client in the same project. +# The partner-api process reads the shared DEVFEED_OIDC_* policy settings; +# Compose maps these dedicated client values into that process only. +# DEVFEED_PARTNER_BASE_URL=https://partner.devfeed.tech +# DEVFEED_PARTNER_API_URL=http://partner-api:8004 +# DEVFEED_PARTNER_PORT=3002 +# DEVFEED_PARTNER_COOKIE_SECURE=true +# DEVFEED_PARTNER_SESSION_TTL_SECONDS=28800 +# DEVFEED_PARTNER_OIDC_CLIENT_ID= +# DEVFEED_PARTNER_OIDC_CLIENT_SECRET= + +# Server-only receipt signing key shared by the public API and user API delivery code. +# Use at least 32 random ASCII characters. Never expose this key to the reader or extensions. +# An empty key disables delivery and collection. See packages/core/PARTNER_PROMOTIONS.md. +DEVFEED_PARTNER_TRACKING_KEY= + +# Dedicated server-only key shared by web and public API for caller quota isolation. +# Use at least 32 random ASCII characters; never reuse the receipt signing key. +DEVFEED_PARTNER_CALLER_KEY= +# Enable only behind an edge that overwrites this single-IP header and restricts origin access. +# Cloudflare Tunnel deployments can use cf-connecting-ip. Leave unset for direct development. +DEVFEED_PARTNER_CALLER_IP_HEADER= + +# Optional partner connector credential, injected into import workers and admin API checks. +# Select this secret reference in the connector's authentication settings. +DEVFEED_PARTNER_SECRET_PRODUCTS_API_TOKEN= +# Encryption key for partner-provided import credentials. Generate a Fernet key +# with cryptography.fernet.Fernet.generate_key(); keep it separate from the DB. +# Inject the same key into partner-api and import workers. Missing configuration +# disables token setup; replacing the key requires replacing saved credentials. +DEVFEED_PARTNER_CREDENTIAL_KEY= diff --git a/.github/api-compatibility/admin-job-kinds.txt b/.github/api-compatibility/admin-job-kinds.txt new file mode 100644 index 000000000..291c382f3 --- /dev/null +++ b/.github/api-compatibility/admin-job-kinds.txt @@ -0,0 +1,19 @@ +# Partner job kinds extend the shared AdminJobOut model. Existing requests retain +# their original kinds: /jobs/{kind} selects only that kind, and fixed analysis, +# notification and topic routes never return partner jobs. Partner kinds require +# an explicit new kind in the request. The generated admin client handles both. +# Scope exceptions to these two values and these exact response properties only. +GET /v1/admin/jobs/ai-analysis added the new `partner-evaluation` enum value to the `items/items/kind` response property for the response status `200` +GET /v1/admin/jobs/ai-analysis added the new `partner-pipeline` enum value to the `items/items/kind` response property for the response status `200` +GET /v1/admin/jobs/{kind} added the new `partner-evaluation` enum value to the `items/items/kind` response property for the response status `200` +GET /v1/admin/jobs/{kind} added the new `partner-pipeline` enum value to the `items/items/kind` response property for the response status `200` +GET /v1/admin/jobs/{kind}/{job_id} added the new `partner-evaluation` enum value to the `kind` response property for the response status `200` +GET /v1/admin/jobs/{kind}/{job_id} added the new `partner-pipeline` enum value to the `kind` response property for the response status `200` +POST /v1/admin/jobs/{kind}/{job_id}/retry added the new `partner-evaluation` enum value to the `kind` response property for the response status `200` +POST /v1/admin/jobs/{kind}/{job_id}/retry added the new `partner-pipeline` enum value to the `kind` response property for the response status `200` +POST /v1/admin/notifications/deliveries/{identifier}/retry added the new `partner-evaluation` enum value to the `kind` response property for the response status `200` +POST /v1/admin/notifications/deliveries/{identifier}/retry added the new `partner-pipeline` enum value to the `kind` response property for the response status `200` +POST /v1/admin/topic-proposals/{proposal_id}/analysis added the new `partner-evaluation` enum value to the `kind` response property for the response status `202` +POST /v1/admin/topic-proposals/{proposal_id}/analysis added the new `partner-pipeline` enum value to the `kind` response property for the response status `202` +POST /v1/admin/topics/{topic_id}/relationships/analysis added the new `partner-evaluation` enum value to the `kind` response property for the response status `202` +POST /v1/admin/topics/{topic_id}/relationships/analysis added the new `partner-pipeline` enum value to the `kind` response property for the response status `202` diff --git a/.github/images.json b/.github/images.json index 860d3b492..333f41e8f 100644 --- a/.github/images.json +++ b/.github/images.json @@ -31,7 +31,9 @@ "apps/notifications/**", "apps/article-enrichment-worker/**" ], - "platforms": ["linux/arm64"], + "platforms": [ + "linux/arm64" + ], "smoke-script": ".github/scripts/smoke-image.sh" }, { @@ -54,7 +56,9 @@ "apps/notifications/**", "apps/images-worker/**" ], - "platforms": ["linux/arm64"], + "platforms": [ + "linux/arm64" + ], "smoke-script": ".github/scripts/smoke-image.sh" }, { @@ -77,7 +81,9 @@ "apps/notifications/**", "apps/source-discovery-worker/**" ], - "platforms": ["linux/arm64"], + "platforms": [ + "linux/arm64" + ], "smoke-script": ".github/scripts/smoke-image.sh" }, { @@ -85,7 +91,9 @@ "image": "ghcr.io/abhi1693/devfeed.tech/mcp", "context": ".", "file": "apps/mcp/Dockerfile", - "platforms": ["linux/arm64"], + "platforms": [ + "linux/arm64" + ], "smoke-script": ".github/scripts/smoke-image.sh" }, { @@ -137,6 +145,26 @@ "linux/arm64" ], "smoke-script": ".github/scripts/smoke-image.sh" + }, + { + "id": "partner-api", + "image": "ghcr.io/abhi1693/devfeed.tech/partner-api", + "context": ".", + "file": "apps/partner-api/Dockerfile", + "platforms": [ + "linux/arm64" + ], + "smoke-script": ".github/scripts/smoke-image.sh" + }, + { + "id": "partner", + "image": "ghcr.io/abhi1693/devfeed.tech/partner", + "context": ".", + "file": "apps/partner/Dockerfile", + "platforms": [ + "linux/arm64" + ], + "smoke-script": ".github/scripts/smoke-image.sh" } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24550d234..28f72d5b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const workspaces = [ 'apps/admin-api', 'apps/aggregator', 'apps/api', 'apps/article-enrichment-worker', - 'apps/cli', 'apps/images-worker', 'apps/mcp', 'apps/notifications', + 'apps/cli', 'apps/images-worker', 'apps/mcp', 'apps/notifications', 'apps/partner-api', 'apps/search-indexer', 'apps/source-discovery-worker', 'apps/user-api', 'packages/core', 'packages/http' ]; @@ -178,6 +178,20 @@ jobs: uv-sync-args: '["--all-packages"]' generated-paths: '["apps/admin/openapi.json","apps/admin/src/lib/api/generated"]' name: Unit / Admin UI lint, tests and build + partner-web: + name: Unit / Partner portal lint, tests, build and browser checks + uses: abhi1693/actions/.github/workflows/node-ci.yml@afba12afb85bf40be9328c92dc53b779f18d5c6d # v1.0.1 + with: + checks: '["lint", "test", "build"]' + scripts: '{"lint": "partner:lint", "test": "ci:partner-test", "build": "ci:partner-browser"}' + timeout-minutes: 20 + runs-on: ubuntu-24.04-arm + node-version: "24" + test-results-path: | + reports/partner.xml + reports/coverage/partner/lcov.info + test-results-name: partner-test-results-${{ github.run_attempt }} + project-env: '{"NEXT_TELEMETRY_DISABLED":"1"}' user-web: uses: abhi1693/actions/.github/workflows/node-ci.yml@afba12afb85bf40be9328c92dc53b779f18d5c6d # v1.0.1 with: @@ -314,6 +328,7 @@ jobs: allow-external-refs: false review: false github-token: "" + err-ignore: .github/api-compatibility/admin-job-kinds.txt output-to-file: reports/api-compatibility/admin.breaking.txt - name: Check public MCP id: mcp_public @@ -705,6 +720,7 @@ jobs: - python-unit - python-integration - admin-web + - partner-web - user-web - browser-budgets - dast @@ -715,7 +731,7 @@ jobs: if: >- always() && !cancelled() && needs.python-unit.result == 'success' && needs.python-integration.result == 'success' && - needs.admin-web.result == 'success' && needs.user-web.result == 'success' && + needs.admin-web.result == 'success' && needs.partner-web.result == 'success' && needs.user-web.result == 'success' && ((github.event_name == 'push' && github.ref == 'refs/heads/master') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && @@ -821,6 +837,7 @@ jobs: needs: - python-unit - admin-web + - partner-web - user-web - extensions - python-integration @@ -853,6 +870,7 @@ jobs: - python-unit - coverage-report - admin-web + - partner-web - user-web - extensions - api-compat @@ -889,7 +907,7 @@ jobs: RESULTS: ${{ toJSON(needs) }} run: | echo "$RESULTS" | jq -e --arg event "$EVENT_NAME" --arg ref_type "$REF_TYPE" --arg comment "$COVERAGE_COMMENT_REQUIRED" --arg browser_comment "$BROWSER_COMMENT_REQUIRED" --arg sonar "$SONAR_EXPECTED" ' - ([.["python-unit"], .["admin-web"], .["user-web"], .extensions, + ([.["python-unit"], .["admin-web"], .["partner-web"], .["user-web"], .extensions, .["python-integration"], .["migration-upgrade"], .["reader-parity"], .["admin-browser"], .["live-browser"], .security] | all(.result == "success")) and diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index f9295e922..23252c951 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -27,15 +27,14 @@ jobs: id: changes run: node scripts/ci/api-performance-changes.mjs measure: - name: ${{ matrix.revision }} sample ${{ matrix.repeat }} + name: Runner pair ${{ matrix.repeat }} needs: select if: github.event_name == 'pull_request' && needs.select.outputs.needed == 'true' runs-on: ubuntu-24.04-arm - timeout-minutes: 10 + timeout-minutes: 20 strategy: fail-fast: false matrix: - revision: [base, head] repeat: [0, 1, 2] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -44,7 +43,6 @@ jobs: path: candidate persist-credentials: false - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: matrix.revision == 'base' with: ref: ${{ github.event.pull_request.base.sha }} path: baseline @@ -59,45 +57,29 @@ jobs: cache-dependency-glob: | candidate/uv.lock baseline/uv.lock - cache-suffix: performance-${{ matrix.revision }} + cache-suffix: performance-pair - name: Install the candidate harness run: sh candidate/scripts/ci/python-bootstrap.sh candidate --editable --all-packages --group loadtest - name: Install the baseline application - if: matrix.revision == 'base' run: sh candidate/scripts/ci/python-bootstrap.sh baseline --editable --all-packages - - name: Run one isolated sample + - name: Run one runner pair env: - APP_ROOT: ${{ matrix.revision == 'base' && 'baseline' || 'candidate' }} - SAMPLE: ${{ matrix.revision }}-${{ matrix.repeat }} - run: | - destination="reports/performance/$SAMPLE" - mkdir -p "$destination" - set +e - candidate/.venv/bin/python candidate/scripts/load-test.py \ - --app-root "$APP_ROOT" --app-python "$APP_ROOT/.venv/bin/python" \ - --users 16 --spawn-rate 4 --seconds 60 --rows 1000 \ - --output "$destination" > "$destination/runner.log" 2>&1 - result=$? - printf '%s\n' "$result" > "$destination/exit-code.txt" - exit "$result" + PAIR_REPEAT: ${{ matrix.repeat }} + run: python3 candidate/scripts/ci/api-performance-pair.py --repeat "$PAIR_REPEAT" - name: Preserve incomplete samples if: always() env: - SAMPLE: ${{ matrix.revision }}-${{ matrix.repeat }} - run: | - mkdir -p "reports/performance/$SAMPLE" - if [ ! -f "reports/performance/$SAMPLE/exit-code.txt" ]; then - echo incomplete > "reports/performance/$SAMPLE/exit-code.txt" - fi + PAIR_REPEAT: ${{ matrix.repeat }} + run: python3 candidate/scripts/ci/api-performance-pair.py --repeat "$PAIR_REPEAT" --preserve-only - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: - name: perf-sample-${{ matrix.revision }}-${{ matrix.repeat }}-${{ github.run_attempt }} + name: perf-sample-pair-${{ matrix.repeat }}-${{ github.run_attempt }} path: reports/performance/ if-no-files-found: error retention-days: 30 compare: - name: Compare six independent samples + name: Compare six samples from three runners needs: [select, measure] if: always() && !cancelled() && github.event_name == 'pull_request' runs-on: ubuntu-24.04 diff --git a/.gitleaksignore b/.gitleaksignore index 155b4c3fe..d00bb5686 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -13,6 +13,6 @@ aefa7143e2d3420c702e54dd1906279f9b9c50ec:docs/development.md:generic-api-key:147 # Public RFC 6455 example WebSocket nonce in the historical transport test. 6f7042198d7a22e1fca9a6e100ed9571853adf8c:infra/codex/transport.test.cjs:generic-api-key:38 -# Disposable partner-placement signing keys used only by historical test fixtures. +# Reviewed disposable fixture literals, replaced by generated test values. 9463cbb56414fc4f83ee6f183873bc39de323d2e:tests/test_partner_placement_api.py:generic-api-key:25 9463cbb56414fc4f83ee6f183873bc39de323d2e:tests/test_partner_placements_integration.py:generic-api-key:44 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01101c6e1..324907175 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,10 +18,10 @@ repos: - id: check-yaml args: [--allow-multiple-documents] - id: end-of-file-fixer - exclude: ^apps/admin/src/lib/api/generated/ + exclude: ^apps/(admin|partner)/src/lib/api/generated/ - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - exclude: ^apps/admin/src/lib/api/generated/ + exclude: ^apps/(admin|partner)/src/lib/api/generated/ - repo: local hooks: @@ -42,7 +42,7 @@ repos: entry: node node_modules/prettier/bin/prettier.cjs --write language: system files: \.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$ - exclude: (^|/)next-env\.d\.ts$|^apps/admin/src/lib/api/generated/ + exclude: (^|/)next-env\.d\.ts$|^apps/(admin|partner)/src/lib/api/generated/ require_serial: true - id: project-checks name: Affected project lint, types and unit tests diff --git a/.prettierignore b/.prettierignore index 3abcb0289..f32b74142 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,3 +14,5 @@ reports/ **/next-env.d.ts apps/admin/openapi.json apps/admin/src/lib/api/generated/ +apps/partner/openapi.json +apps/partner/src/lib/api/generated/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fbaeee6fe..515fe9459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,23 @@ Application releases and Alembic schema revisions are separate identifiers. - Label the user list's activity timestamp as Last active, reflecting authenticated activity rather than only sign-ins. +### Enhancements + +- Set a separate sync interval for each partner without interrupting running syncs. +- Browse partner products in a searchable table with dedicated detail pages and related listings, jobs, and evaluations. +- Sync partner products independently so a failed product can retry without repeating successful work. +- Connect launch platform partnerships to sync developer products automatically, check product capabilities, and privately evaluate article relevance. +- Browse partnership pipeline jobs and evaluations in dedicated pages with related records, matching evidence, and worker logs. +- Manage partners in dedicated list, create, edit, and detail pages, with related products and sync job logs. +- Combine matching products from multiple launch platforms while retaining each listing, attribution, and shared product exclusions. + +### Fixes + +- Search partner products, pipeline jobs, and evaluations using literal text, including punctuation. +- Recover partner syncs after repeated failures so corrected upstream products can be discovered. +- Refresh article matching after retrying product checks while an earlier evaluation is still active. +- Sort products by name or update time in a partner's related objects. + ## 0.0.48 — 2026-10-02 ### Fixes diff --git a/Dockerfile b/Dockerfile index 5a163dc55..db78eef8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ COPY packages/http/pyproject.toml packages/http/pyproject.toml COPY apps/api/pyproject.toml apps/api/pyproject.toml COPY apps/mcp/pyproject.toml apps/mcp/pyproject.toml COPY apps/admin-api/pyproject.toml apps/admin-api/pyproject.toml +COPY apps/partner-api/pyproject.toml apps/partner-api/pyproject.toml COPY apps/user-api/pyproject.toml apps/user-api/pyproject.toml COPY apps/aggregator/pyproject.toml apps/aggregator/pyproject.toml COPY apps/notifications/pyproject.toml apps/notifications/pyproject.toml diff --git a/README.md b/README.md index 5a8be5645..5337f2b73 100644 --- a/README.md +++ b/README.md @@ -103,3 +103,11 @@ or [join the discussion](https://github.com/abhi1693/devfeed.tech/discussions). Contributions to the reading experience, feed compatibility, accessibility, and documentation are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md). + +## Partner portal + +The standalone partner portal and API expose membership-scoped product/ad analytics, +and partnership tiers. Superusers manage accounts in the admin application. See [portal setup and access model](apps/partner/README.md). + +Configure product API endpoints, mappings, pagination and authentication in the admin app. +See [partner API connectors](packages/core/PARTNER_CONNECTORS.md) for setup and supported contracts. diff --git a/apps/admin-api/Dockerfile b/apps/admin-api/Dockerfile index 291877a6d..1286b01b2 100644 --- a/apps/admin-api/Dockerfile +++ b/apps/admin-api/Dockerfile @@ -15,6 +15,7 @@ COPY packages/http/pyproject.toml packages/http/pyproject.toml COPY apps/api/pyproject.toml apps/api/pyproject.toml COPY apps/mcp/pyproject.toml apps/mcp/pyproject.toml COPY apps/admin-api/pyproject.toml apps/admin-api/pyproject.toml +COPY apps/partner-api/pyproject.toml apps/partner-api/pyproject.toml COPY apps/user-api/pyproject.toml apps/user-api/pyproject.toml COPY apps/aggregator/pyproject.toml apps/aggregator/pyproject.toml COPY apps/notifications/pyproject.toml apps/notifications/pyproject.toml diff --git a/apps/admin-api/pyproject.toml b/apps/admin-api/pyproject.toml index 7bd187b07..4d049db26 100644 --- a/apps/admin-api/pyproject.toml +++ b/apps/admin-api/pyproject.toml @@ -4,7 +4,7 @@ version = "0.0.52" description = "Isolated DevFeed administration and OIDC service" requires-python = ">=3.12" dependencies = [ - "devfeed-core", "devfeed-http[identity]", "fastapi>=0.142.2,<1", "uvicorn[standard]>=0.34,<1", + "devfeed-core", "devfeed-http[identity,uploads]", "fastapi>=0.142.2,<1", "uvicorn[standard]>=0.34,<1", "websockets>=15,<18", "rq>=2.8,<3", ] diff --git a/apps/admin-api/src/articles.py b/apps/admin-api/src/articles.py index c036f6b83..3a32454bc 100644 --- a/apps/admin-api/src/articles.py +++ b/apps/admin-api/src/articles.py @@ -24,6 +24,8 @@ ArticleReview, ArticleTag, ArticleTopic, + PartnerAsset, + PartnerCampaign, Source, ) from devfeed_core.schemas import ( @@ -39,14 +41,9 @@ TagOut, ) from devfeed_core.services import OperationConflict, RecordNotFound +from devfeed_core.text_search import text_search from devfeed_core.urls import canonicalize_url, fingerprint, validate_public_url -from fastapi import APIRouter, Depends, HTTPException, Response -from pydantic import Field, field_validator -from sqlalchemy import delete, select - -from devfeed_admin_api.auth import Admin, require_admin -from devfeed_admin_api.dependencies import DB -from devfeed_admin_api.pagination import ( +from devfeed_http.pagination import ( Listing, Page, paginate, @@ -54,7 +51,12 @@ record, require_record, ) -from devfeed_admin_api.search import text_search +from fastapi import APIRouter, Depends, HTTPException, Response +from pydantic import Field, field_validator +from sqlalchemy import delete, select + +from devfeed_admin_api.auth import Admin, require_admin +from devfeed_admin_api.dependencies import DB router = APIRouter( prefix="/v1/admin/articles", tags=["admin-articles"], dependencies=[Depends(require_admin)] @@ -394,6 +396,14 @@ def remove(article_id: uuid.UUID, session: DB) -> Response: for model in (ArticleImageJob, ArticleEnrichmentJob, ArticleAnalysisJob) ], ) + if any( + session.scalar(select(select(model.id).where(model.article_id == article_id).exists())) + for model in (PartnerCampaign, PartnerAsset) + ): + raise OperationConflict( + "Article has sponsorship history and cannot be deleted; keep it unpublished " + "to preserve campaign attribution" + ) session.execute(delete(Article).where(Article.id == article_id)) session.commit() logger.info("article_deleted", extra={"article_id": log_identifier(article_id)}) diff --git a/apps/admin-api/src/automation.py b/apps/admin-api/src/automation.py index 1429df738..1002db904 100644 --- a/apps/admin-api/src/automation.py +++ b/apps/admin-api/src/automation.py @@ -32,8 +32,10 @@ from devfeed_core.research_evidence import VERIFICATION_VERSION from devfeed_core.schemas import InputModel, ORMModel from devfeed_core.services import OperationConflict +from devfeed_core.text_search import text_search from devfeed_core.topic_decision_metrics import TopicDecisionMetrics, decision_metrics from devfeed_core.topics import lock_topics +from devfeed_http.pagination import Listing, Page, paginate, record, require_record from fastapi import APIRouter, Depends from pydantic import BaseModel, Field from sqlalchemy import ( @@ -54,8 +56,6 @@ from devfeed_admin_api.auth import Admin, require_admin from devfeed_admin_api.dependencies import DB -from devfeed_admin_api.pagination import Listing, Page, paginate, record, require_record -from devfeed_admin_api.search import text_search router = APIRouter( prefix="/v1/admin/automation", tags=["admin-automation"], dependencies=[Depends(require_admin)] diff --git a/apps/admin-api/src/job_views.py b/apps/admin-api/src/job_views.py index d9d1e9e58..b8aa0b5bb 100644 --- a/apps/admin-api/src/job_views.py +++ b/apps/admin-api/src/job_views.py @@ -21,6 +21,9 @@ "article_id", "topic_id", "source_id", + "product_id", + "account_id", + "subject_revision", "operation", "http_status", "outcome", diff --git a/apps/admin-api/src/jobs.py b/apps/admin-api/src/jobs.py index decf97bcb..d5655736e 100644 --- a/apps/admin-api/src/jobs.py +++ b/apps/admin-api/src/jobs.py @@ -13,6 +13,9 @@ ArticleAnalysisJob, ArticleImageJob, NotificationDelivery, + PartnerEvaluation, + PartnerPipelineJob, + PartnerProduct, Source, SourceDiscoveryJob, Topic, @@ -20,6 +23,8 @@ TopicProposal, ) from devfeed_core.schemas import ORMModel +from devfeed_core.text_search import text_search +from devfeed_http.pagination import Listing, Page, paginate, record from devfeed_http.schemas import ErrorResponse from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.encoders import jsonable_encoder @@ -31,11 +36,14 @@ from devfeed_admin_api.auth import Admin, actor, require_admin from devfeed_admin_api.dependencies import DB, get_redis from devfeed_admin_api.job_views import JOB_FIELDS -from devfeed_admin_api.pagination import Listing, Page, paginate, record -from devfeed_admin_api.search import text_search MODELS = {kind: d.model for kind, d in JOB_DEFINITIONS.items() if d.admin_visible} -LOG_MODELS = {**MODELS, "source-discovery": SourceDiscoveryJob} +LOG_MODELS = { + **MODELS, + "source-discovery": SourceDiscoveryJob, + "partner-evaluation": PartnerEvaluation, + "partner-pipeline": PartnerPipelineJob, +} router = APIRouter( prefix="/v1/admin/jobs", tags=["admin-jobs"], dependencies=[Depends(require_admin)] ) @@ -51,7 +59,14 @@ def retry_candidates(model): if model is NotificationDelivery: return select(model.id).where(model.status == "failed") subject = ( - (model.article_id, model.topic_id, model.source_id) + ( + model.article_id, + model.topic_id, + model.source_id, + model.product_id, + model.account_id, + case((model.product_id.is_not(None), model.operation), else_=null()), + ) if model is ArticleImageJob else (model.proposal_id, model.topic_id) if model is TopicAnalysisJob @@ -85,6 +100,7 @@ class AdminJobOut(ORMModel): proposal_id: uuid.UUID | None = None topic_id: uuid.UUID | None = None article_id: uuid.UUID | None = None + product_id: uuid.UUID | None = None target_name: str | None = None error: str | None details: dict[str, JsonValue] @@ -248,6 +264,29 @@ def ai_analysis_jobs( } +def job_metadata(model): + """Resolve retry eligibility and subject names without browser request fan-out.""" + metadata = select(model.id, retry_candidate(model)) + if model is ArticleImageJob: + return ( + metadata.add_columns( + func.coalesce(Source.name, Topic.name, Article.title, PartnerProduct.name) + ) + .outerjoin(Source, Source.id == model.source_id) + .outerjoin(Topic, Topic.id == model.topic_id) + .outerjoin(Article, Article.id == model.article_id) + .outerjoin(PartnerProduct, PartnerProduct.id == model.product_id) + ) + subject = ( + Source if hasattr(model, "source_id") else Article if hasattr(model, "article_id") else None + ) + if subject is None: + return metadata.add_columns(null()) + field = "source_id" if subject is Source else "article_id" + label = Source.name if subject is Source else Article.title + return metadata.add_columns(label).outerjoin(subject, subject.id == getattr(model, field)) + + @router.get("/{kind}", response_model=Page[AdminJobOut], operation_id="admin_jobs_list") def jobs( kind: JobKind, @@ -302,25 +341,7 @@ def jobs( ) # Resolve labels alongside retry eligibility for this page: one query, # rather than one full source/article request per row in the browser. - subject = ( - Source if hasattr(model, "source_id") else Article if hasattr(model, "article_id") else None - ) - metadata = select(model.id, retry_candidate(model)) - if model is ArticleImageJob: - metadata = ( - metadata.add_columns(func.coalesce(Source.name, Topic.name, Article.title)) - .outerjoin(Source, Source.id == model.source_id) - .outerjoin(Topic, Topic.id == model.topic_id) - .outerjoin(Article, Article.id == model.article_id) - ) - elif subject is not None: - field = "source_id" if subject is Source else "article_id" - label = Source.name if subject is Source else Article.title - metadata = metadata.add_columns(label).outerjoin( - subject, subject.id == getattr(model, field) - ) - else: - metadata = metadata.add_columns(null()) + metadata = job_metadata(model) rows = ( session.execute(metadata.where(model.id.in_([item.id for item in result["items"]]))).all() if result["items"] @@ -346,10 +367,8 @@ def detail(kind: JobKind, job_id: uuid.UUID, session: DB): raise HTTPException(404, "Job not found") model = MODELS[kind] job = record(session, model, job_id) - retryable = bool( - session.scalar(select(model.id).where(model.id == job_id, retry_candidate(model))) - ) - return job_view(job, kind, retryable=retryable) + _, retryable, name = session.execute(job_metadata(model).where(model.id == job_id)).one() + return job_view(job, kind, retryable=bool(retryable)).model_copy(update={"target_name": name}) @router.post("/{kind}/{job_id}/retry", response_model=AdminJobOut, operation_id="admin_job_retry") diff --git a/apps/admin-api/src/main.py b/apps/admin-api/src/main.py index f22bd4a22..8c158b335 100644 --- a/apps/admin-api/src/main.py +++ b/apps/admin-api/src/main.py @@ -9,6 +9,10 @@ from devfeed_core.db import database_revision from devfeed_core.logging import configure_logging from devfeed_core.version import SCHEMA_REVISION, __version__ +from devfeed_http.body_limits import RequestBodyLimitMiddleware +from devfeed_http.image_uploads import ImageUploadBodyLimit +from devfeed_http.partner_ad_images import ad_image_upload_path +from devfeed_http.partner_products import product_body_limit from devfeed_http.schemas import ERROR_RESPONSES from devfeed_http.service import HTTPService from devfeed_http.telemetry import fastapi_telemetry @@ -24,6 +28,9 @@ notifications, overview, overview_panels, + partner_accounts, + partner_jobs, + partner_tools, search_analytics, search_suggestions, source_imports, @@ -85,6 +92,8 @@ def create_app() -> FastAPI: app.state.overview_snapshots = {} app.state.overview_tasks = {} app.state.overview_retry_at = {} + app.add_middleware(ImageUploadBodyLimit, path_predicate=ad_image_upload_path) + app.add_middleware(RequestBodyLimitMiddleware, limits={}, limit_for=product_body_limit) # No cross-origin cookie access: the Next.js admin service proxies same-origin requests. service.configure(app, settings, private=True) @@ -103,6 +112,9 @@ def create_app() -> FastAPI: ai_connection.router, overview.router, overview_panels.router, + partner_accounts.router, + partner_jobs.router, + partner_tools.router, automation.router, taxonomy.router, topic_proposals.router, diff --git a/apps/admin-api/src/oidc.py b/apps/admin-api/src/oidc.py index 71017845d..0c5dcd355 100644 --- a/apps/admin-api/src/oidc.py +++ b/apps/admin-api/src/oidc.py @@ -11,9 +11,9 @@ from devfeed_http.oidc import consume_callback_flow as consume_callback_flow from devfeed_http.oidc import discovery as discovery from devfeed_http.oidc import validate_callback_response as validate_callback_response +from devfeed_http.roles import verified_roles from devfeed_admin_api.config import Settings -from devfeed_admin_api.roles import verified_roles def configured(settings: Settings) -> bool: diff --git a/apps/admin-api/src/partner_accounts.py b/apps/admin-api/src/partner_accounts.py new file mode 100644 index 000000000..7a1bf3abb --- /dev/null +++ b/apps/admin-api/src/partner_accounts.py @@ -0,0 +1,615 @@ +"""Partner account management belongs exclusively to the authenticated admin API.""" + +import uuid +from datetime import UTC, datetime +from typing import Annotated + +from devfeed_core.models import ( + PartnerAccount, + PartnerAsset, + PartnerDailyMetric, + PartnerMembership, + UserAccount, +) +from devfeed_core.partner_ad_formats import AdFormat, validate_account_formats +from devfeed_core.partner_audiences import ( + AudienceDimension, + audience_topic_options, + validate_audience, +) +from devfeed_core.partner_promotions import ( + request_promotion_enrollment, +) +from devfeed_core.partner_tiers import TIER_BENEFITS, promotion_kind_allowed, tier_policy +from devfeed_core.text_search import text_search +from devfeed_http.partner_ad_images import ( + limit_ad_image_upload, + lock_image_account, + lock_image_product, + save_product_promotion_with_images, + upload_product_ad_image, +) +from devfeed_http.partner_campaigns import CampaignService +from devfeed_http.partner_portal import ( + AccountInput, + AccountOut, + AccountsOut, + AssetInput, + AssetOut, + AudienceTopicsOut, + CampaignInput, + CampaignOut, + CampaignsOut, + CreativePreviewOut, + Dashboard, + DestinationPreviewOut, + InventoryOut, + MemberInput, + MemberOut, + MemberSubject, + MetricInput, + PartnershipTierOut, + ProductPromotionInput, + ProductPromotionOut, + SponsorArticlesOut, + asset_access, + creative_preview, + destination_preview, + inventory_products, + list_product_promotions, + product_promotion, + reporting_dashboard, + sponsor_articles, + validate_product, +) +from devfeed_http.partner_products import ( + ManualProductInput, + ManualProductUpdate, + ProductDetailOut, + archive_manual_product, + create_manual_product, + limit_product_write, + lock_manual_identity, + product_detail, + update_manual_product, +) +from fastapi import APIRouter, Depends, File, HTTPException, Query, Response, UploadFile +from sqlalchemy import func, select +from sqlalchemy.dialects.postgresql import insert + +from devfeed_admin_api.auth import Admin, AdminIdentity, require_admin +from devfeed_admin_api.dependencies import DB, get_redis + +router = APIRouter( + prefix="/v1/admin/partner-accounts", + tags=["admin-partner-accounts"], + dependencies=[Depends(require_admin)], +) + + +def require_superuser(admin: Admin) -> AdminIdentity: + if "superuser" not in admin.roles: + raise HTTPException(403, "Superuser access required") + return admin + + +Superuser = Annotated[AdminIdentity, Depends(require_superuser)] + + +def account_access(session, _admin, account_id, *, lock=False): + query = select(PartnerAccount).where(PartnerAccount.id == account_id) + if lock: + query = query.with_for_update().execution_options(populate_existing=True) + account = session.scalar(query) + if account is None: + raise HTTPException(404, "Partner account not found") + return account + + +def write_manual_account(session, admin, account_id, *, product_id=None, url=None): + account_access(session, admin, account_id) + product = lock_manual_identity(session, account_id, product_id=product_id, url=url) + return account_access(session, admin, account_id, lock=True), product + + +@router.post( + "/{account_id}/products", + response_model=ProductDetailOut, + status_code=201, + operation_id="adminPartnerProductCreate", +) +def create_product( + account_id: uuid.UUID, payload: ManualProductInput, session: DB, superuser: Superuser +): + account_access(session, superuser, account_id) + limit_product_write(get_redis(), superuser, account_id) + account, existing = write_manual_account( + session, superuser, account_id, url=payload.product_url + ) + result = create_manual_product(session, account, payload, existing) + session.commit() + return result + + +@router.get( + "/{account_id}/products/{product_id:uuid}", + response_model=ProductDetailOut, + operation_id="adminPartnerProductGet", +) +def get_product(account_id: uuid.UUID, product_id: uuid.UUID, session: DB, superuser: Superuser): + return product_detail(session, account_access(session, superuser, account_id), product_id) + + +@router.patch( + "/{account_id}/products/{product_id:uuid}", + response_model=ProductDetailOut, + operation_id="adminPartnerProductUpdate", +) +def update_product( + account_id: uuid.UUID, + product_id: uuid.UUID, + payload: ManualProductUpdate, + session: DB, + superuser: Superuser, +): + account_access(session, superuser, account_id) + limit_product_write(get_redis(), superuser, account_id) + account, product = write_manual_account(session, superuser, account_id, product_id=product_id) + result = update_manual_product(session, account, product, payload) + session.commit() + return result + + +@router.delete( + "/{account_id}/products/{product_id:uuid}", + status_code=204, + response_class=Response, + operation_id="adminPartnerProductArchive", +) +def archive_product( + account_id: uuid.UUID, product_id: uuid.UUID, session: DB, superuser: Superuser +): + account_access(session, superuser, account_id) + limit_product_write(get_redis(), superuser, account_id) + account, product = write_manual_account(session, superuser, account_id, product_id=product_id) + archive_manual_product(session, account, product) + session.commit() + return Response(status_code=204) + + +@router.get("/tiers", response_model=list[PartnershipTierOut]) +def partnership_tiers(_admin: Superuser): + return [ + {"tier": tier, "benefits": list(benefits), **tier_policy(tier)} + for tier, benefits in TIER_BENEFITS.items() + ] + + +@router.get("", response_model=AccountsOut) +def accounts( + session: DB, + _admin: Superuser, + limit: int = Query(100, ge=1, le=200), + offset: int = Query(0, ge=0), + q: str = Query("", max_length=200, pattern=r"^[^\x00]*$"), +): + filters = [text_search(q.strip(), PartnerAccount.name)] if q.strip() else [] + return { + "items": session.scalars( + select(PartnerAccount) + .where(*filters) + .order_by(PartnerAccount.name, PartnerAccount.id) + .limit(limit) + .offset(offset) + ).all(), + "total": session.scalar(select(func.count()).select_from(PartnerAccount).where(*filters)), + } + + +@router.get( + "/audience-topics", + response_model=AudienceTopicsOut, + operation_id="adminPartnerAudienceTopicsCatalog", +) +def audience_catalog( + session: DB, + _admin: Superuser, + dimension: AudienceDimension, + q: str = Query("", max_length=200, pattern=r"^[^\x00]*$"), + topic_id: uuid.UUID | None = None, + offset: int = Query(0, ge=0), + limit: int = Query(25, ge=1, le=100), +): + return audience_topic_options( + session, dimension, q=q, topic_id=topic_id, offset=offset, limit=limit + ) + + +@router.get( + "/sponsor-articles", + response_model=SponsorArticlesOut, + operation_id="adminPartnerSponsorArticlesCatalog", +) +def sponsor_article_catalog( + session: DB, + _admin: Superuser, + q: str = Query("", max_length=200, pattern=r"^[^\x00]*$"), + article_id: uuid.UUID | None = None, + offset: int = Query(0, ge=0), + limit: int = Query(25, ge=1, le=100), +): + return sponsor_articles(session, q=q, article_id=article_id, offset=offset, limit=limit) + + +@router.get( + "/{account_id}/sponsor-articles", + response_model=SponsorArticlesOut, + operation_id="adminPartnerSponsorArticles", +) +def account_sponsor_articles( + account_id: uuid.UUID, + session: DB, + admin: Superuser, + q: str = Query("", max_length=200, pattern=r"^[^\x00]*$"), + article_id: uuid.UUID | None = None, + offset: int = Query(0, ge=0), + limit: int = Query(25, ge=1, le=100), +): + account_access(session, admin, account_id) + return sponsor_articles(session, q=q, article_id=article_id, offset=offset, limit=limit) + + +@router.get( + "/{account_id}/creative-preview", + response_model=CreativePreviewOut, + operation_id="adminPartnerCreativePreview", +) +def preview_creative( + account_id: uuid.UUID, session: DB, admin: Superuser, product_id: uuid.UUID | None = None +): + return creative_preview( + session, account_access(session, admin, account_id), product_id=product_id + ) + + +@router.get( + "/{account_id}/audience-topics", + response_model=AudienceTopicsOut, + operation_id="adminPartnerAudienceTopics", +) +def audience_topics( + account_id: uuid.UUID, + session: DB, + admin: Superuser, + dimension: AudienceDimension, + q: str = Query("", max_length=200, pattern=r"^[^\x00]*$"), + topic_id: uuid.UUID | None = None, + offset: int = Query(0, ge=0), + limit: int = Query(25, ge=1, le=100), +): + account = account_access(session, admin, account_id) + return audience_topic_options( + session, dimension, account=account, q=q, topic_id=topic_id, offset=offset, limit=limit + ) + + +@router.get("/{account_id}/dashboard", response_model=Dashboard) +def dashboard( + account_id: uuid.UUID, + session: DB, + admin: Superuser, + days: int = Query(30, ge=1, le=365), + limit: int = Query(100, ge=1, le=200), + offset: int = Query(0, ge=0), + ad_format: AdFormat | None = None, +): + return reporting_dashboard( + account_id, + session, + account_access(session, admin, account_id), + days, + limit, + offset, + ad_format=ad_format, + ) + + +@router.get( + "/{account_id}/destination-preview", + response_model=DestinationPreviewOut, + operation_id="adminPartnerDestinationPreview", +) +def preview_destination(account_id: uuid.UUID, session: DB, admin: Superuser): + return destination_preview(session, account_access(session, admin, account_id)) + + +@router.post("", response_model=AccountOut, status_code=201) +def create_account(payload: AccountInput, _superuser: Superuser, session: DB): + try: + validate_account_formats(payload.tier, payload.ad_formats) + validate_audience(session, payload.tier, payload.audience) + except ValueError as exc: + raise HTTPException(422, str(exc)) from None + account = PartnerAccount(**payload.model_dump(mode="json")) + session.add(account) + session.commit() + return account + + +@router.put("/{account_id}", response_model=AccountOut) +def update_account(account_id: uuid.UUID, payload: AccountInput, superuser: Superuser, session: DB): + account = account_access(session, superuser, account_id, lock=True) + try: + validate_account_formats(payload.tier, payload.ad_formats, previous=account.ad_formats) + validate_audience(session, payload.tier, payload.audience, previous=account.audience) + except ValueError as exc: + raise HTTPException(422, str(exc)) from None + for key, value in payload.model_dump(mode="json").items(): + setattr(account, key, value) + session.flush() + request_promotion_enrollment(session, account_id) + session.commit() + return account + + +@router.get("/{account_id}/members", response_model=list[MemberOut]) +def members(account_id: uuid.UUID, superuser: Superuser, session: DB): + account_access(session, superuser, account_id) + rows = session.execute( + select(PartnerMembership, UserAccount) + .outerjoin( + UserAccount, + (UserAccount.issuer == PartnerMembership.issuer) + & (UserAccount.subject == PartnerMembership.subject) + & (UserAccount.organization_id == superuser.organization_id), + ) + .where(PartnerMembership.account_id == account_id) + .order_by(PartnerMembership.subject) + ).all() + return [ + MemberOut( + issuer=membership.issuer, + subject=membership.subject, + user_id=user.id if user else None, + name=user.name if user else None, + email=user.email if user else None, + ) + for membership, user in rows + ] + + +@router.put("/{account_id}/members", status_code=204, response_class=Response) +def add_member(account_id: uuid.UUID, payload: MemberInput, superuser: Superuser, session: DB): + account_access(session, superuser, account_id) + user = session.get(UserAccount, payload.user_id) + if ( + user is None + or user.issuer != superuser.issuer + or user.organization_id != superuser.organization_id + ): + raise HTTPException(404, "User not found in this identity organization") + # Resolve immutable identity on the server; clients cannot supply a forged subject. + session.execute( + insert(PartnerMembership) + .values(account_id=account_id, issuer=user.issuer, subject=user.subject) + .on_conflict_do_nothing() + ) + session.commit() + return Response(status_code=204) + + +@router.delete("/{account_id}/members/{subject}", status_code=204, response_class=Response) +def remove_member(account_id: uuid.UUID, subject: MemberSubject, superuser: Superuser, session: DB): + account_access(session, superuser, account_id) + member = session.get(PartnerMembership, (account_id, superuser.issuer, subject)) + if member is not None: + session.delete(member) + session.commit() + return Response(status_code=204) + + +@router.post("/{account_id}/assets", response_model=AssetOut, status_code=201) +def create_asset(account_id: uuid.UUID, payload: AssetInput, superuser: Superuser, session: DB): + account = account_access(session, superuser, account_id, lock=True) + if not promotion_kind_allowed(account, payload.kind): + raise HTTPException(422, "Promotion type is not included in this partnership tier") + validate_product(payload, session) + asset = PartnerAsset(account_id=account_id, **payload.model_dump()) + session.add(asset) + session.commit() + return asset + + +@router.put("/{account_id}/assets/{asset_id}", response_model=AssetOut) +def update_asset( + account_id: uuid.UUID, + asset_id: uuid.UUID, + payload: AssetInput, + superuser: Superuser, + session: DB, +): + account = account_access(session, superuser, account_id, lock=True) + if not promotion_kind_allowed(account, payload.kind): + raise HTTPException(422, "Promotion type is not included in this partnership tier") + asset = asset_access(session, account_id, asset_id) + validate_product(payload, session) + if asset.automatic and (asset.product_id != payload.product_id or asset.kind != payload.kind): + raise HTTPException( + 409, "Automatic attribution is immutable; use account defaults or campaigns" + ) + for key, value in payload.model_dump().items(): + setattr(asset, key, value) + session.commit() + return asset + + +@router.put("/{account_id}/assets/{asset_id}/metrics", status_code=204, response_class=Response) +def record_metrics( + account_id: uuid.UUID, + asset_id: uuid.UUID, + payload: MetricInput, + superuser: Superuser, + session: DB, +): + account_access(session, superuser, account_id) + asset_access(session, account_id, asset_id) + # Replace the day's trusted totals atomically, so retrying an import cannot double count. + values = {**payload.model_dump(), "updated_at": datetime.now(UTC)} + session.execute( + insert(PartnerDailyMetric) + .values(asset_id=asset_id, **values) + .on_conflict_do_update( + index_elements=[PartnerDailyMetric.asset_id, PartnerDailyMetric.day], set_=values + ) + ) + session.commit() + return Response(status_code=204) + + +@router.get( + "/{account_id}/campaigns", response_model=CampaignsOut, operation_id="adminPartnerCampaignsList" +) +def campaigns( + account_id: uuid.UUID, + session: DB, + superuser: Superuser, + limit: int = Query(100, ge=1, le=200), + offset: int = Query(0, ge=0), +): + account = account_access(session, superuser, account_id) + return CampaignService(session, account).page(limit=limit, offset=offset) + + +@router.get( + "/{account_id}/campaigns/{campaign_id}", + response_model=CampaignOut, + operation_id="adminPartnerCampaignGet", +) +def campaign(account_id: uuid.UUID, campaign_id: uuid.UUID, session: DB, superuser: Superuser): + account = account_access(session, superuser, account_id) + return CampaignService(session, account).get(campaign_id) + + +@router.post( + "/{account_id}/campaigns", + response_model=CampaignOut, + status_code=201, + operation_id="adminPartnerCampaignCreate", +) +def create_campaign( + account_id: uuid.UUID, payload: CampaignInput, session: DB, superuser: Superuser +): + account = account_access(session, superuser, account_id, lock=True) + campaign = CampaignService(session, account).save(payload) + session.commit() + return campaign + + +@router.put( + "/{account_id}/campaigns/{campaign_id}", + response_model=CampaignOut, + operation_id="adminPartnerCampaignUpdate", +) +def update_campaign( + account_id: uuid.UUID, + campaign_id: uuid.UUID, + payload: CampaignInput, + session: DB, + superuser: Superuser, +): + account = account_access(session, superuser, account_id, lock=True) + service = CampaignService(session, account) + campaign = service.save(payload, campaign_id=campaign_id) + session.commit() + return service.reports([campaign])[0] + + +@router.get( + "/{account_id}/inventory", response_model=InventoryOut, operation_id="adminPartnerInventoryList" +) +def inventory( + account_id: uuid.UUID, + session: DB, + superuser: Superuser, + limit: int = Query(100, ge=1, le=200), + offset: int = Query(0, ge=0), + q: str = Query("", max_length=200, pattern=r"^[^\x00]*$"), + product_id: uuid.UUID | None = None, + include_archived: bool = False, +): + account = account_access(session, superuser, account_id) + return inventory_products( + session, + account, + q=q, + product_id=product_id, + offset=offset, + limit=limit, + include_archived=include_archived, + ) + + +@router.get( + "/{account_id}/products/promotions", + response_model=list[ProductPromotionOut], + operation_id="adminPartnerProductPromotionsList", +) +def product_promotions(account_id: uuid.UUID, session: DB, superuser: Superuser): + account_access(session, superuser, account_id) + return list_product_promotions(session, account_id) + + +@router.put( + "/{account_id}/products/{product_id}/promotion", + response_model=ProductPromotionOut, + operation_id="adminPartnerProductPromotionUpdate", +) +def update_product_promotion( + account_id: uuid.UUID, + product_id: uuid.UUID, + payload: ProductPromotionInput, + session: DB, + superuser: Superuser, +): + account_access(session, superuser, account_id) + lock_image_product(session, account_id, product_id) + account = lock_image_account( + session, select(PartnerAccount).where(PartnerAccount.id == account_id) + ) + if account is None: + raise HTTPException(404, "Partner account not found") + return save_product_promotion_with_images(session, account, product_id, payload) + + +@router.get( + "/{account_id}/products/{product_id}/promotion", + response_model=ProductPromotionOut, + operation_id="adminPartnerProductPromotionGet", +) +def get_product_promotion( + account_id: uuid.UUID, product_id: uuid.UUID, session: DB, superuser: Superuser +): + return product_promotion(session, account_access(session, superuser, account_id), product_id) + + +@router.post( + "/{account_id}/products/{product_id}/ad-image", + response_model=ProductPromotionOut, + operation_id="adminPartnerProductAdImageUpload", +) +def upload_ad_image( + account_id: uuid.UUID, + product_id: uuid.UUID, + file: Annotated[UploadFile, File(description="JPEG, PNG or WebP, up to 5 MB")], + session: DB, + superuser: Superuser, +): + account_access(session, superuser, account_id) + limit_ad_image_upload(get_redis(), superuser, account_id) + lock_image_product(session, account_id, product_id) + account = lock_image_account( + session, select(PartnerAccount).where(PartnerAccount.id == account_id) + ) + if account is None: + raise HTTPException(404, "Partner account not found") + return upload_product_ad_image(session, account, product_id, file) diff --git a/apps/admin-api/src/partner_jobs.py b/apps/admin-api/src/partner_jobs.py new file mode 100644 index 000000000..206dae449 --- /dev/null +++ b/apps/admin-api/src/partner_jobs.py @@ -0,0 +1,256 @@ +"""Read-only administration pages for partner pipeline and evaluation runs.""" + +import uuid +from datetime import datetime +from typing import Literal + +from devfeed_core.models import ( + PartnerEvaluation, + PartnerListing, + PartnerPipelineJob, + PartnerProduct, +) +from devfeed_core.partner_connections import PartnerJobOut +from devfeed_core.partner_tools import EvaluationOut +from devfeed_core.schemas import ORMModel +from devfeed_http.pagination import Listing, Page, paginate, record +from fastapi import APIRouter, Depends +from sqlalchemy import String, cast, exists, or_, select +from sqlalchemy.orm import load_only + +from devfeed_admin_api.auth import require_admin +from devfeed_admin_api.dependencies import DB +from devfeed_admin_api.partner_tools import evaluation_view + +router = APIRouter( + prefix="/v1/admin/partner-tools", + tags=["admin-partner-tools"], + dependencies=[Depends(require_admin)], +) +JobStatus = Literal["queued", "running", "succeeded", "failed"] + + +class PartnerPipelineOut(PartnerJobOut): + available_at: datetime + product_name: str | None + + +class PartnerEvaluationSummary(ORMModel): + id: uuid.UUID + product_id: uuid.UUID + product_name: str + status: str + attempts: int + created_at: datetime + available_at: datetime + finished_at: datetime | None + error: str | None + matches: int + articles: int + + +class PartnerEvaluationDetail(EvaluationOut): + attempts: int + available_at: datetime + + +def pipeline_views(session, jobs): + names = ( + dict( + session.execute( + select(PartnerProduct.id, PartnerProduct.name).where( + PartnerProduct.id.in_([job.product_id for job in jobs if job.product_id]) + ) + ).all() + ) + if jobs + else {} + ) + return [ + { + **PartnerJobOut.model_validate(job).model_dump(), + "available_at": job.available_at, + "product_name": names.get(job.product_id), + } + for job in jobs + ] + + +@router.get( + "/pipeline", response_model=Page[PartnerPipelineOut], operation_id="admin_partner_pipeline_list" +) +def pipeline( + session: DB, + query: Listing, + status: JobStatus | None = None, + operation: Literal["sync", "sync_product", "assess"] | None = None, + provider: str | None = None, + product_id: uuid.UUID | None = None, + parent_id: uuid.UUID | None = None, +): + statement = ( + select(PartnerPipelineJob) + .options( + load_only( + *(getattr(PartnerPipelineJob, key) for key in PartnerJobOut.model_fields), + PartnerPipelineJob.available_at, + raiseload=True, + ) + ) + .outerjoin(PartnerProduct, PartnerProduct.id == PartnerPipelineJob.product_id) + ) + for column, value in ( + (PartnerPipelineJob.status, status), + (PartnerPipelineJob.operation, operation), + (PartnerPipelineJob.product_id, product_id), + (PartnerPipelineJob.parent_id, parent_id), + ): + if value is not None: + statement = statement.where(column == value) + if provider is not None: + statement = statement.where( + or_( + PartnerPipelineJob.provider == provider, + (PartnerPipelineJob.operation == "assess") + & exists( + select(PartnerListing.id).where( + PartnerListing.provider == provider, + PartnerListing.product_id == PartnerPipelineJob.product_id, + ) + ), + ) + ) + if query.q: + statement = statement.where( + or_( + *[ + column.icontains(query.q, autoescape=True) + for column in ( + cast(PartnerPipelineJob.id, String), + PartnerPipelineJob.external_id, + PartnerPipelineJob.provider, + PartnerProduct.name, + ) + ] + ) + ) + page = paginate( + session, + statement, + query, + { + "created_at": PartnerPipelineJob.created_at, + "status": PartnerPipelineJob.status, + "attempts": PartnerPipelineJob.attempts, + }, + "-created_at", + ) + return {**page, "items": pipeline_views(session, page["items"])} + + +@router.get( + "/pipeline/{job_id}", + response_model=PartnerPipelineOut, + operation_id="admin_partner_pipeline_get", +) +def pipeline_detail(job_id: uuid.UUID, session: DB): + return pipeline_views(session, [record(session, PartnerPipelineJob, job_id)])[0] + + +@router.get( + "/evaluations", + response_model=Page[PartnerEvaluationSummary], + operation_id="admin_partner_evaluations_list", +) +def evaluations( + session: DB, + query: Listing, + status: JobStatus | None = None, + product_id: uuid.UUID | None = None, + provider: str | None = None, +): + # Lists omit article snapshots; the detail endpoint supplies the full evidence. + fields = ( + PartnerEvaluation.id, + PartnerEvaluation.product_id, + PartnerEvaluation.status, + PartnerEvaluation.attempts, + PartnerEvaluation.created_at, + PartnerEvaluation.available_at, + PartnerEvaluation.finished_at, + PartnerEvaluation.error, + PartnerEvaluation.result, + ) + name = PartnerEvaluation.snapshot["product"]["name"].astext + statement = select(PartnerEvaluation).options(load_only(*fields, raiseload=True)) + if status is not None: + statement = statement.where(PartnerEvaluation.status == status) + if product_id is not None: + statement = statement.where(PartnerEvaluation.product_id == product_id) + if provider is not None: + statement = statement.where( + exists( + select(PartnerListing.id).where( + PartnerListing.product_id == PartnerEvaluation.product_id, + PartnerListing.provider == provider, + ) + ) + ) + if query.q: + statement = statement.where( + or_( + cast(PartnerEvaluation.id, String).icontains(query.q, autoescape=True), + name.icontains(query.q, autoescape=True), + ) + ) + page = paginate( + session, + statement, + query, + { + "created_at": PartnerEvaluation.created_at, + "status": PartnerEvaluation.status, + "attempts": PartnerEvaluation.attempts, + }, + "-created_at", + ) + names: dict[uuid.UUID, str | None] = ( + { + identifier: product_name + for identifier, product_name in session.execute( + select(PartnerEvaluation.id, name).where( + PartnerEvaluation.id.in_([job.id for job in page["items"]]) + ) + ).all() + } + if page["items"] + else {} + ) + return { + **page, + "items": [ + { + **{field.key: getattr(job, field.key) for field in fields if field.key != "result"}, + "product_name": names.get(job.id) or "Product", + "matches": sum( + bool(d.get("relevant")) for d in (job.result or {}).get("decisions", []) + ), + "articles": len((job.result or {}).get("decisions", [])), + } + for job in page["items"] + ], + } + + +@router.get( + "/evaluations/{job_id}", + response_model=PartnerEvaluationDetail, + operation_id="admin_partner_evaluation_get", +) +def evaluation_detail(job_id: uuid.UUID, session: DB): + job = record(session, PartnerEvaluation, job_id) + return { + **evaluation_view(session, job), + "attempts": job.attempts, + "available_at": job.available_at, + } diff --git a/apps/admin-api/src/partner_tools.py b/apps/admin-api/src/partner_tools.py new file mode 100644 index 000000000..ce63e63cc --- /dev/null +++ b/apps/admin-api/src/partner_tools.py @@ -0,0 +1,479 @@ +"""Import connections and direct partner inventory, private to administrators.""" + +import logging +import uuid +from typing import Literal + +from devfeed_core.job_lifecycle import fail_or_retry +from devfeed_core.models import ( + PartnerAccount, + PartnerConnection, + PartnerEvaluation, + PartnerListing, + PartnerPipelineJob, + PartnerProduct, + utcnow, +) +from devfeed_core.partner_catalog import ( + has_sources, + listing_views, + lock_catalog, + product_has_active_source, +) +from devfeed_core.partner_connections import ( + ConnectionAction, + ConnectionCreate, + ConnectionOut, + ConnectionSettings, + ConnectorPreview, + ConnectorPreviewOut, + ConnectorResponseOut, + PartnerJobOut, + ProductAction, + configure_connection, + connection_view, + connection_views, + request_assessment, + update_connection_definition, +) +from devfeed_core.partner_connectors import ( + CONNECTOR_PRESETS, + ConnectorConfig, + connector_snapshot, +) +from devfeed_core.partner_providers import PARTNER_PRESETS, PartnerProviderOut +from devfeed_core.partner_tools import ( + EvaluationOut, + ProductOut, + manual_product_views, + product_view, + snapshot_current, +) +from devfeed_core.text_search import text_search +from devfeed_http.pagination import Listing, Page, paginate, record +from devfeed_http.partner_connector_previews import ConnectorPreviewService +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import String, cast, exists, or_, select +from sqlalchemy.dialects.postgresql import insert + +from devfeed_admin_api.auth import Admin, actor, require_admin +from devfeed_admin_api.dependencies import DB + +logger = logging.getLogger(__name__) + +router = APIRouter( + prefix="/v1/admin/partner-tools", + tags=["admin-partner-tools"], + dependencies=[Depends(require_admin)], +) + + +@router.get( + "/connections", + response_model=Page[ConnectionOut], + operation_id="admin_partner_connections_list", +) +def connections(query: Listing, session: DB): + statement = select(PartnerConnection).where(PartnerConnection.archived_at.is_(None)) + if query.q: + statement = statement.where( + text_search(query.q, PartnerConnection.provider, PartnerConnection.name) + ) + result = paginate( + session, + statement, + query, + {"provider": PartnerConnection.provider, "name": PartnerConnection.name}, + default="provider", + ) + result["items"] = connection_views(session, result["items"]) + return result + + +@router.get( + "/connections/{provider}", + response_model=ConnectionOut, + operation_id="admin_partner_connection_get", +) +def connection(provider: str, session: DB): + value = session.get(PartnerConnection, provider) + if value is None or value.archived_at is not None: + raise HTTPException(404, "Import connection not found") + return connection_view(session, value) + + +@router.get( + "/providers", + response_model=list[PartnerProviderOut], + operation_id="admin_partner_providers_list", +) +def providers(): + return list(PARTNER_PRESETS.values()) + + +@router.post( + "/connector-preview", + response_model=ConnectorPreviewOut, + operation_id="admin_partner_connector_preview", +) +def preview_connector(body: ConnectorPreview, admin: Admin): + return ConnectorPreviewService(body.provider, body.connector).products() + + +@router.post( + "/connector-response", + response_model=ConnectorResponseOut, + operation_id="admin_partner_connector_response", +) +def connector_response(body: ConnectorPreview, admin: Admin): + return ConnectorPreviewService(body.provider, body.connector).response() + + +def log_connection(event, connection, admin, affected_jobs): + logger.info( + event, + extra={ + "provider": connection.provider, + "partnership_type": connection.partnership_type, + "connection_revision": connection.revision, + "account_id": str(connection.account_id) if connection.account_id else None, + "enabled": connection.enabled, + "sync_interval_minutes": connection.sync_interval_minutes, + "actor_subject": admin.subject, + "job_ids": affected_jobs, + "job_id": affected_jobs[0] if connection.enabled and affected_jobs else None, + "jobs_cancelled": len(affected_jobs) if not connection.enabled else 0, + }, + ) + + +@router.post( + "/connections", + response_model=ConnectionOut, + status_code=201, + operation_id="admin_partner_connection_create", +) +def create_connection(body: ConnectionCreate, session: DB, admin: Admin): + lock_catalog(session) + if body.connector is None and body.provider not in CONNECTOR_PRESETS: + raise HTTPException(422, "Provide an API connector definition") + if body.name is None and body.provider not in PARTNER_PRESETS: + raise HTTPException(422, "Provide an import connection name") + if session.get(PartnerConnection, body.provider): + raise HTTPException(409, "This import connection already exists; edit its settings") + if body.account_id is not None and session.get(PartnerAccount, body.account_id) is None: + raise HTTPException(422, "Partner account not found") + if body.connector is not None and body.connector.auth.source == "connection": + raise HTTPException(422, "Configure account credentials in the partner portal") + connection = PartnerConnection( + account_id=body.account_id, + provider=body.provider, + name=body.name or PARTNER_PRESETS[body.provider].name, + connector=( + body.connector or ConnectorConfig.model_validate(CONNECTOR_PRESETS[body.provider]) + ).model_dump(mode="json"), + enabled=False, + updated_by=actor(admin), + sync_interval_minutes=body.sync_interval_minutes, + ) + session.add(connection) + session.flush() + affected_jobs = configure_connection(session, connection, body.enabled) + session.commit() + log_connection("partner_connection_added", connection, admin, affected_jobs) + return connection_view(session, connection) + + +@router.put( + "/connections/{provider}", + response_model=ConnectionOut, + operation_id="admin_partner_connection_update", +) +def update_connection(provider: str, body: ConnectionSettings, session: DB, admin: Admin): + lock_catalog(session) + connection = session.get(PartnerConnection, provider) + if not connection or connection.archived_at is not None: + raise HTTPException(404, "Import connection not found") + if connection.revision != body.expected_revision: + raise HTTPException( + 409, "Import connection settings changed; reopen the form to load the latest settings" + ) + if "account_id" in body.model_fields_set and body.account_id != connection.account_id: + if connection.encrypted_credential: + raise HTTPException( + 409, "Remove the account-bound credential before moving this connection" + ) + if body.account_id is not None and session.get(PartnerAccount, body.account_id) is None: + raise HTTPException(422, "Partner account not found") + connection.account_id = body.account_id + connection.revision += 1 + definition_changed = False + if body.name is not None and body.name != connection.name: + connection.name = body.name + connection.revision += 1 + if body.connector is not None: + current = ConnectorConfig.model_validate(connector_snapshot(connection)) + if body.connector.auth.source == "connection" and body.connector.auth.mode != "none": + if not connection.encrypted_credential or ( + current.base_url != body.connector.base_url or current.auth != body.connector.auth + ): + raise HTTPException( + 409, "Update the account-bound credential in the partner portal" + ) + elif body.connector.auth.mode == "none": + connection.encrypted_credential = None + definition_changed = update_connection_definition(session, connection, body.connector) + affected_jobs = configure_connection( + session, + connection, + body.enabled, + sync_interval_minutes=body.sync_interval_minutes, + sync=definition_changed, + ) + connection.updated_by = actor(admin) + session.commit() + log_connection("partner_connection_settings_updated", connection, admin, affected_jobs) + return connection_view(session, connection) + + +@router.post( + "/connections/{provider}", + response_model=ConnectionOut, + operation_id="admin_partner_connection_action", +) +def connection_action(provider: str, body: ConnectionAction, session: DB, admin: Admin): + lock_catalog(session) + if not session.get(PartnerConnection, provider): + if provider not in PARTNER_PRESETS: + raise HTTPException(404, "Import connection not found") + session.execute( + insert(PartnerConnection) + .values( + provider=provider, + name=PARTNER_PRESETS[provider].name, + connector=ConnectorConfig.model_validate(CONNECTOR_PRESETS[provider]).model_dump( + mode="json" + ), + ) + .on_conflict_do_nothing() + ) + connection = session.scalar( + select(PartnerConnection).where(PartnerConnection.provider == provider).with_for_update() + ) + assert connection is not None + if connection.archived_at is not None: + raise HTTPException(404, "Import connection not found") + if body.action == "sync" and not connection.enabled: + raise HTTPException(409, "Enable this import connection before syncing") + affected_jobs = configure_connection( + session, connection, body.action != "pause", sync=body.action != "pause" + ) + connection.updated_by = actor(admin) + session.commit() + log_connection( + { + "connect": "partner_connection_connected", + "pause": "partner_connection_paused", + "sync": "partner_sync_requested", + }[body.action], + connection, + admin, + affected_jobs, + ) + return connection_view(session, connection) + + +@router.get( + "/connections/{provider}/jobs", + response_model=Page[PartnerJobOut], + operation_id="admin_partner_connection_jobs", +) +def connection_jobs(provider: str, session: DB, query: Listing): + connection = session.get(PartnerConnection, provider) + if connection is None or connection.archived_at is not None: + raise HTTPException(404, "Import connection not found") + shared_product = exists( + select(PartnerListing.id).where( + PartnerListing.provider == provider, + PartnerListing.product_id == PartnerPipelineJob.product_id, + ) + ) + statement = select(PartnerPipelineJob).where( + or_( + PartnerPipelineJob.provider == provider, + (PartnerPipelineJob.operation == "assess") & shared_product, + ) + ) + if query.q: + statement = statement.where( + or_( + *[ + column.icontains(query.q, autoescape=True) + for column in ( + cast(PartnerPipelineJob.id, String), + PartnerPipelineJob.external_id, + PartnerPipelineJob.provider, + ) + ] + ) + ) + return paginate( + session, + statement, + query, + { + "created_at": PartnerPipelineJob.created_at, + "status": PartnerPipelineJob.status, + }, + "-created_at", + ) + + +@router.get("", response_model=Page[ProductOut], operation_id="admin_partner_tools_list") +def listing( + session: DB, + query: Listing, + provider: str | None = None, + product_id: uuid.UUID | None = None, + status: Literal["pending", "approved", "rejected", "paused", "withdrawn"] | None = None, +): + statement = select(PartnerProduct).where(PartnerProduct.merged_into_id.is_(None), has_sources()) + if provider is not None: + statement = statement.where( + exists( + select(PartnerListing.id).where( + PartnerListing.product_id == PartnerProduct.id, + PartnerListing.provider == provider, + ) + ) + ) + if status is not None: + statement = statement.where(PartnerProduct.status == status) + if product_id is not None: + statement = statement.where(PartnerProduct.id == product_id) + if query.q: + statement = statement.where(PartnerProduct.name.icontains(query.q, autoescape=True)) + page = paginate( + session, + statement, + query, + {"name": PartnerProduct.name, "updated_at": PartnerProduct.updated_at}, + "-updated_at", + ) + listings = listing_views(session, [p.id for p in page["items"]]) + manual = manual_product_views(session, [p.id for p in page["items"]]) + return { + **page, + "items": [product_view(p, listings[p.id], manual[p.id]) for p in page["items"]], + } + + +@router.post( + "/{product_id}/actions", response_model=ProductOut, operation_id="admin_partner_product_action" +) +def product_action(product_id: uuid.UUID, body: ProductAction, session: DB, admin: Admin): + lock_catalog(session) + product = record(session, PartnerProduct, product_id) + if product.merged_into_id or not session.scalar( + select(PartnerProduct.id).where(PartnerProduct.id == product.id, has_sources()) + ): + raise HTTPException(404, "Partner product not found") + if body.action == "retry" and not product_has_active_source(session, product_id): + raise HTTPException(409, "An active product source is required for rechecking") + pipeline_jobs = session.scalars( + select(PartnerPipelineJob) + .where( + PartnerPipelineJob.product_id == product_id, + PartnerPipelineJob.status.in_(["queued", "running"]), + ) + .with_for_update() + ).all() + evaluations = session.scalars( + select(PartnerEvaluation) + .where( + PartnerEvaluation.product_id == product_id, + PartnerEvaluation.status.in_(["queued", "running"]), + ) + .with_for_update() + ).all() + for job in pipeline_jobs: + fail_or_retry(job, "Product action superseded this check", utcnow(), retryable=False) + for evaluation in evaluations: + fail_or_retry(evaluation, "Product action superseded this check", utcnow(), retryable=False) + product = session.scalar( + select(PartnerProduct) + .where(PartnerProduct.id == product_id) + .with_for_update() + .execution_options(populate_existing=True) + ) + assert product is not None + if product.revision != body.expected_revision: + raise HTTPException(409, "Product changed; refresh and try again") + if product.status == "withdrawn" and body.action == "retry": + raise HTTPException(409, "This product no longer has an active source") + product.excluded = body.action == "exclude" + product.status = "paused" if product.excluded else "pending" + product.revision += 1 + product.verified_at = None + product.updated_at = utcnow() + product.reviews = [ + *product.reviews, + {"actor": actor(admin), "action": body.action, "note": "", "at": utcnow().isoformat()}, + ] + session.flush() + if not product.excluded: + request_assessment(session, product) + session.commit() + logger.info( + "partner_product_excluded" if product.excluded else "partner_product_recheck_requested", + extra={ + "product_id": str(product.id), + "product_revision": product.revision, + "actor_subject": admin.subject, + }, + ) + return product_view( + product, + listing_views(session, [product.id])[product.id], + manual_product_views(session, [product.id])[product.id], + ) + + +def evaluation_view(session, job): + return { + **{ + key: getattr(job, key) + for key in ( + "id", + "product_id", + "status", + "created_at", + "finished_at", + "error", + "snapshot", + "reviews", + ) + }, + "result": job.result or None, + "current": snapshot_current(session, job), + } + + +@router.get( + "/{product_id}/evaluations", + response_model=list[EvaluationOut], + operation_id="admin_partner_tools_evaluations", +) +def evaluations(product_id: uuid.UUID, session: DB): + product = record(session, PartnerProduct, product_id) + if product.merged_into_id or not session.scalar( + select(PartnerProduct.id).where(PartnerProduct.id == product.id, has_sources()) + ): + raise HTTPException(404, "Partner product not found") + jobs = session.scalars( + select(PartnerEvaluation) + .where(PartnerEvaluation.product_id == product_id) + .order_by(PartnerEvaluation.created_at.desc()) + .limit(10) + ).all() + return [evaluation_view(session, job) for job in jobs] diff --git a/apps/admin-api/src/sources.py b/apps/admin-api/src/sources.py index 0544cf6e4..2704548c0 100644 --- a/apps/admin-api/src/sources.py +++ b/apps/admin-api/src/sources.py @@ -30,7 +30,15 @@ ) from devfeed_core.source_preview import preview_source from devfeed_core.source_types import SourceType +from devfeed_core.text_search import text_search from devfeed_core.urls import validate_public_url +from devfeed_http.pagination import ( + Listing, + Page, + paginate, + record, + require_record, +) from fastapi import APIRouter, Depends, HTTPException, Response from pydantic import Field, field_validator from sqlalchemy import delete, select @@ -38,14 +46,6 @@ from devfeed_admin_api.auth import Admin, require_admin from devfeed_admin_api.dependencies import DB -from devfeed_admin_api.pagination import ( - Listing, - Page, - paginate, - record, - require_record, -) -from devfeed_admin_api.search import text_search from devfeed_admin_api.source_solver import solve_feed router = APIRouter( diff --git a/apps/admin-api/src/taxonomy.py b/apps/admin-api/src/taxonomy.py index 6e8c111ba..65191bd12 100644 --- a/apps/admin-api/src/taxonomy.py +++ b/apps/admin-api/src/taxonomy.py @@ -9,13 +9,13 @@ TagPatch, TagWrite, ) +from devfeed_core.text_search import text_search +from devfeed_http.pagination import Listing, Page, paginate, prohibit_references, record from fastapi import APIRouter, Depends, Response from sqlalchemy import delete, select from devfeed_admin_api.auth import require_admin from devfeed_admin_api.dependencies import DB -from devfeed_admin_api.pagination import Listing, Page, paginate, prohibit_references, record -from devfeed_admin_api.search import text_search router = APIRouter( prefix="/v1/admin", tags=["admin-taxonomy"], dependencies=[Depends(require_admin)] diff --git a/apps/admin-api/src/topic_proposals.py b/apps/admin-api/src/topic_proposals.py index 3850c842f..8878670e7 100644 --- a/apps/admin-api/src/topic_proposals.py +++ b/apps/admin-api/src/topic_proposals.py @@ -9,6 +9,7 @@ from devfeed_core.config import get_settings from devfeed_core.models import TopicAnalysisJob, TopicProposal from devfeed_core.schemas import DatabaseText +from devfeed_core.text_search import text_search from devfeed_core.topic_analysis import ( FIELDS, TopicAnalysisBatchOut, @@ -16,6 +17,7 @@ request_topic_analysis, ) from devfeed_core.topics import lock_topics +from devfeed_http.pagination import Listing, Page, paginate, record from fastapi import APIRouter, Depends, HTTPException, Query, Response from pydantic import BaseModel from sqlalchemy import delete, func, or_, select @@ -24,8 +26,6 @@ from devfeed_admin_api.auth import Admin, actor, require_admin from devfeed_admin_api.dependencies import DB from devfeed_admin_api.jobs import AdminJobOut, job_view -from devfeed_admin_api.pagination import Listing, Page, paginate, record -from devfeed_admin_api.search import text_search router = APIRouter(prefix="/v1/admin", tags=["admin-topics"], dependencies=[Depends(require_admin)]) diff --git a/apps/admin-api/src/topic_relationships.py b/apps/admin-api/src/topic_relationships.py index 83c9e5742..05d1a33dc 100644 --- a/apps/admin-api/src/topic_relationships.py +++ b/apps/admin-api/src/topic_relationships.py @@ -7,6 +7,7 @@ from devfeed_core.models import Topic, TopicRelation, TopicRelationProposal from devfeed_core.schemas import ORMModel from devfeed_core.services import OperationConflict +from devfeed_core.text_search import text_search, topic_search from devfeed_core.topic_relationships import ( RelationKind, RelationshipAnalysisRequest, @@ -18,14 +19,13 @@ review_relationship, ) from devfeed_core.topics import lock_topics +from devfeed_http.pagination import Listing, Page, paginate, record from fastapi import APIRouter, Depends, HTTPException, Query, Response from sqlalchemy import UUID, cast, func, literal, or_, select, union_all from devfeed_admin_api.auth import Admin, require_admin from devfeed_admin_api.dependencies import DB from devfeed_admin_api.jobs import AdminJobOut, job_view -from devfeed_admin_api.pagination import Listing, Page, paginate, record -from devfeed_admin_api.search import text_search, topic_search from devfeed_admin_api.topic_proposals import actor router = APIRouter(prefix="/v1/admin", tags=["admin-topics"], dependencies=[Depends(require_admin)]) diff --git a/apps/admin-api/src/topic_replacements.py b/apps/admin-api/src/topic_replacements.py index 2b8c791c9..308c96d2e 100644 --- a/apps/admin-api/src/topic_replacements.py +++ b/apps/admin-api/src/topic_replacements.py @@ -4,13 +4,13 @@ from devfeed_core.models import Topic, TopicProposal from devfeed_core.schemas import ORMModel +from devfeed_core.text_search import text_search +from devfeed_http.pagination import Listing, Page from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import UUID, String, cast, func, literal, or_, select, union_all from devfeed_admin_api.auth import require_admin from devfeed_admin_api.dependencies import DB -from devfeed_admin_api.pagination import Listing, Page -from devfeed_admin_api.search import text_search router = APIRouter(prefix="/v1/admin", tags=["admin-topics"], dependencies=[Depends(require_admin)]) diff --git a/apps/admin-api/src/topics.py b/apps/admin-api/src/topics.py index 6bd03ec62..c7895a1af 100644 --- a/apps/admin-api/src/topics.py +++ b/apps/admin-api/src/topics.py @@ -15,6 +15,7 @@ ) from devfeed_core.schemas import ORMModel from devfeed_core.services import OperationConflict, RecordNotFound +from devfeed_core.text_search import text_search, topic_search from devfeed_core.topic_deletion import TopicDeleteImpact, delete_topic, deletion_impact from devfeed_core.topics import ( RelationWrite, @@ -24,13 +25,12 @@ relate_topics, save_topic, ) +from devfeed_http.pagination import Listing, Page, paginate, prohibit_references, record from fastapi import APIRouter, Depends, Response from sqlalchemy import or_, select from devfeed_admin_api.auth import Admin, require_admin from devfeed_admin_api.dependencies import DB -from devfeed_admin_api.pagination import Listing, Page, paginate, prohibit_references, record -from devfeed_admin_api.search import text_search, topic_search from devfeed_admin_api.topic_proposals import actor router = APIRouter(prefix="/v1/admin", tags=["admin-topics"], dependencies=[Depends(require_admin)]) diff --git a/apps/admin-api/src/users.py b/apps/admin-api/src/users.py index 3adbe63e8..aa06c0c38 100644 --- a/apps/admin-api/src/users.py +++ b/apps/admin-api/src/users.py @@ -10,6 +10,8 @@ Article, ArticleBookmark, ArticleLike, + PartnerAccount, + PartnerMembership, Source, Topic, UserAccount, @@ -29,6 +31,7 @@ from devfeed_core.must_reads import read_snapshot from devfeed_core.recommendations import request_recommendation_refresh from devfeed_core.services import OperationConflict +from devfeed_core.text_search import text_search from devfeed_core.user_settings import ( DevCardSettings, FeedSettings, @@ -40,14 +43,13 @@ from devfeed_core.user_settings import ( UserReadingStreak as ReadingStreakSettings, ) +from devfeed_http.pagination import Listing, Page, paginate, record, require_record from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from sqlalchemy import String, cast, func, select -from devfeed_admin_api.auth import require_admin +from devfeed_admin_api.auth import Admin, require_admin from devfeed_admin_api.dependencies import DB -from devfeed_admin_api.pagination import Listing, Page, paginate, record, require_record -from devfeed_admin_api.search import text_search router = APIRouter( prefix="/v1/admin/users", tags=["admin-users"], dependencies=[Depends(require_admin)] @@ -83,7 +85,13 @@ def from_account(cls, account): ) +class AdminUserPartnerAccount(BaseModel): + id: uuid.UUID + name: str + + class AdminUserDetail(AdminUserOut): + partner_accounts: list[AdminUserPartnerAccount] = Field(default_factory=list) sign_in_name: str | None interests: int recommendations: int @@ -208,12 +216,18 @@ class AdminUserRecommendation(BaseModel): def users( session: DB, query: Listing, + admin: Admin, + identity_only: bool = False, interests: Literal["following", "liked", "none"] | None = None, ): name = func.coalesce( UserAccount.profile["display_name"].astext, UserAccount.name, "Unnamed user" ) statement = select(UserAccount) + if identity_only: + statement = statement.where( + UserAccount.issuer == admin.issuer, UserAccount.organization_id == admin.organization_id + ) if query.q: statement = statement.where( text_search( @@ -349,7 +363,19 @@ def user(user_id: uuid.UUID, session: DB): "expired" if state.expires_at is None or state.expires_at <= utcnow() else "ready" ) ) + partner_accounts = session.scalars( + select(PartnerAccount) + .join(PartnerMembership, PartnerMembership.account_id == PartnerAccount.id) + .where( + PartnerMembership.issuer == account.issuer, + PartnerMembership.subject == account.subject, + ) + .order_by(PartnerAccount.name, PartnerAccount.id) + ).all() return AdminUserDetail( + partner_accounts=[ + AdminUserPartnerAccount(id=item.id, name=item.name) for item in partner_accounts + ], **AdminUserOut.from_account(account).model_dump( exclude={ "followed_topics", diff --git a/apps/admin/Dockerfile b/apps/admin/Dockerfile index 7f3ef523f..e188793fe 100644 --- a/apps/admin/Dockerfile +++ b/apps/admin/Dockerfile @@ -5,7 +5,9 @@ WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 COPY package.json package-lock.json ./ COPY apps/admin/package.json apps/admin/package.json +COPY apps/partner/package.json apps/partner/package.json COPY apps/web/package.json apps/web/package.json +COPY packages/http/package.json packages/http/package.json COPY packages/theme/package.json packages/theme/package.json COPY packages/ui/package.json packages/ui/package.json COPY packages/telemetry/package.json packages/telemetry/package.json diff --git a/apps/admin/README.md b/apps/admin/README.md index b60a36542..9c3682935 100644 --- a/apps/admin/README.md +++ b/apps/admin/README.md @@ -7,6 +7,20 @@ Traefik ingress also redirects HTTP to HTTPS. See [configuration, authentication, service boundaries and commands](../../docs/admin.md). +## Sidebar navigation + +The administration sidebar starts as a compact icon rail. Use **Expand sidebar** at the +bottom to reveal group and link labels; **Collapse sidebar** restores the rail. The saved +preference belongs to this browser's admin interface and is separate from reader and +partner navigation. Active routes remain marked in either view. + +On narrow screens, **Navigation** opens an inline menu. Selecting a link or pressing Escape +closes it. Long navigation scrolls independently while the desktop expand/collapse control +stays reachable, including on short screens. + +Validate with `npm run admin:test` and, after `npm run admin:build`, +`node apps/admin/tests/browser/overview.mjs`. + ## User details The user Details tab shows reader-profile fields and Dev Card styling only when @@ -89,6 +103,213 @@ legacy defaults, saved decisions, and desktop/mobile layout. Screenshots: [desktop](screenshots/article-page-kind-desktop.png), [mobile](screenshots/article-page-kind-mobile.png). +## Product import connections + +**Partner accounts** represent companies or commercial relationships. They own tiers, portal +memberships, import acceptance and promotion settings. **Import connections** describe how to +import a catalog from a platform API: configuration, credentials, enabled state and sync schedule. +Select the required **Partner account** when creating or editing an import connection. One account +may own several connections; each connection belongs to one account. Legacy or API-created rows +can remain unassigned for compatibility. The connector is the endpoint, mapping, authentication +and pagination definition inside a connection. + +Open **Partnerships → Import connections**, choose **Create import connection**, and select a +supported preset or **Custom API** in the **Platform** field. Use the Enabled setting to start +or pause automatic syncing. Each connection has a details +page and a separate edit form; synced products appear under **Partnerships → Products**. +The connection pages use `/partnerships/connections`; old `/partnerships/partners` links redirect +there. Account pages remain under `/partnerships/accounts`. +Nick Launches and ShipyardHQ have creation presets. ShipyardHQ maps `productUrl` to the +product website, `url` to the platform listing, and `pricing.model` to pricing; its bearer +secret reference is `DEVFEED_PARTNER_SECRET_PRODUCTS_API_TOKEN`. Existing saved connector +definitions remain explicit when editing an import connection. +Products use the shared admin table with search, sorting, pagination, and column preferences. +Open a product for its description, assessment, capability evidence, and record information. +Its **Related objects** tab shows each platform's listing and attribution alongside pipeline +jobs and evaluations. Retry checks and exclusion remain available on the product detail page. +The import connection detail page uses the shared record information panels and a **Related objects** +tab with paginated products, pipeline jobs, and evaluations. Open a run for its detail page. +**Partnerships → Pipeline jobs** lists discovery, individual product syncs, and qualification +runs with status, operation, search, sorting, and pagination. Runs link to their parent, +child jobs, import connection, product, and dedicated logs page. +Discovery and product syncs use the `ingestion` queue consumed by the standard background +worker. They do not require the optional source-discovery worker. Qualification and +article matching use the AI workers. +**Partnerships → Evaluations** lists article-matching runs. Each run has Details, Related +objects, Results, and Logs tabs. Results show article decisions, reasoning, and source +quotes, and indicate when product or article changes made the results outdated. +**Jobs → Enrichment → Images** also lists product featured-image and logo runs, identifies +their operation, and links to the product. Their retry histories are independent for each +product and operation; article, source, and topic runs retain their existing retry rules. +The provider catalog comes from the API. A platform can be added once; edits use revision +checks to prevent overwriting another administrator’s settings. +DevFeed pulls developer products directly from the platform API. Set **Sync interval +(minutes)** when creating or editing each import connection: 1–10,080 minutes, defaulting to 360 +(six hours). Interval edits recalculate the next sync from the last successful sync; an +overdue schedule becomes due immediately. If no sync has succeeded, the interval starts +from the edit time. Active discovery and product jobs continue, and completion schedules +the next run using the latest interval. Disabled connections retain their interval without +scheduling work. **Sync now** refreshes early; disabling an import connection in **Edit** stops synchronization +and invalidates its in-flight sync jobs. Shared product checks can continue through another enabled connection. No product creation, editing, JSON upload, manual approval, +or article-ID entry is available. + +Import connections identify both the partnership type (`launch_platform`) and provider +(`nick-launches`, or another configured REST JSON connector). Connect an import source to +its partner account once so its products inherit the account's promotion defaults. A +connection alone does not establish a paid agreement. External ad networks and billing +integrations remain separate from product imports. + +### Promotion defaults and paid campaigns + +Open **Partnerships → Partner accounts → Edit** to configure **Default promotion**, +**Imported product acceptance**, **Product click destination**, automatic inclusion, +and allowed article/topic/feed placements. Promotion type +inherits the tier unless explicitly overridden: Bronze defaults to product +recommendations; Silver, Gold, Platinum, and Diamond default to sponsored placements and also allow +an ordinary recommendation override. Changing tier updates inherited promotion immediately. +**Trust connected partner imports** accepts resolved imports for the account without waiting +for AI capability checks. Capability assessments remain separate and continue in the background. +Every displayed product placement carries a **Sponsored** disclosure and cannot be dismissed, +regardless of tier or campaign. Products have no per-reader exposure limit or repeat cooldown; +the reader ignores previous product hiding preferences and deduplicates products within a page. + +The account's **Products** tab lists its imported catalog with search and pagination. +Products inherit account defaults and the selected acceptance policy. The table shows +account acceptance separately from qualification. Exclude a product to block its +ordinary and sponsored appearances for this account; allowing it again restores its +inherited policy. Global qualification exclusions require rechecking the canonical +product. If automatic inclusion is off, only a campaign's explicit product selection can +opt a product into delivery. + +Sponsored defaults create an **Inherited sponsored placement** standing campaign automatically; +manage it through account defaults. Silver and higher tiers can use **Campaigns → Create paid +campaign** for additional dated promotions. Cover the whole +eligible catalog, including newly qualified imports, choose named products from the +account's catalog, or filter products by approved topics and verified technologies. +When both filter dimensions are supplied, a product must match one topic and one +technology. Set finite start/end dates in local time, status, placement surfaces, +priority, and an optional campaign-wide daily impression limit. Relevance ranks sponsors +first; priority breaks ties between equally relevant campaigns. Account pauses, product +exclusions, qualification, +and relevance continue to gate delivery. Contextual matches rank first. Sponsored defaults +can fill remaining slots in the unfiltered main feed; search, category, article, and topic +placements require a relevant match. Expiring an additional campaign restores account defaults. +Dated campaigns on lower tiers remain available for reporting; sponsored delivery and campaign +changes require Silver or higher. + +Silver and higher tiers support Feed, Homepage sponsorship, Sponsored content and Engagement. +Automatic formats default to Feed; choose additional channels in account defaults or partner +preferences. Sponsored content requires a campaign with a currently approved visible article and +an eligible account product. The article itself must share active approved topics with the feed; +untagged or unrelated stories are not delivered. It never publishes or rewrites the article. Its sponsor disclosure +remains after the campaign ends. Homepage sponsorship is main-feed-only; Engagement and sponsored +articles always require contextual relevance. Newsletter and Agent Ads are not included. +Product settings restrict formats and locations across automatic and explicit campaigns. Inherit +uses the account policy, while an empty allowlist blocks that dimension; tier and exclusions win. + +Platinum and Diamond add **Audience targeting** for experience, programming languages and +developer tools. Account settings are inherited by default; dated campaigns can inherit, use a +custom audience, or explicitly target all readers. Choices within a dimension are alternatives; +each populated dimension must match. Product filters still select inventory independently. +Audience criteria use a signed-in reader's optional private experience choice, current stack, +follows and fresh valid interests, and apply to both relevant and fallback placements. Anonymous +readers cannot receive targeted inventory. Downgrades preserve criteria and stop targeted delivery +rather than widening it. Unavailable selected topics remain visible for review but prevent +delivery. Lower tiers can explicitly clear criteria and cannot add populated criteria. + +**Product click destination** chooses the product website or the connected partner’s listing; +both include DevFeed UTM attribution. Partners can update this preference in their portal. +Selecting the partner listing omits products without that listing rather than changing their destination. + +Campaign lists show impressions, clicks, and click-through rates. Automatic placement +reporting records link back to their account defaults or campaign, preserving attribution. +Historical manual assets remain accessible under **Related objects**; the normal workflow +does not require associating every product as a placement or ad. + +Validate account defaults, inclusion/exclusion, all/selected/filtered campaigns, scoped +product pickers, and desktop/mobile layouts with `npm run admin:test` and, after +`npm run admin:build`, `node apps/admin/tests/browser/partner-accounts.mjs`. + +The adapter follows cursor pagination, accepts the documented and live response envelopes, +normalizes the platform listing and product website URLs, and selects the Developer Tools +category. Discovery saves its page cursor and queues one durable job per product. Each +product job fetches that product's API endpoint and commits it separately; a failed product +cannot roll back its neighbors. Successful jobs are not rerun when another product retries. +The next scheduled sync or **Sync now** resumes a failed run once at its saved cursor, +retrying failed products without replaying successful ones. If that generation fails again, +the next sync starts a fresh API scan so corrected or removed upstream entries can recover. +Duplicate listings within a run share one product job. +Product changes trigger new checks. Missing products are withdrawn only after a +complete successful scan and successful product jobs; failures preserve existing listings. Withdrawal cleanup runs in batches of 25. Legacy products without an API connection remain hidden and ineligible. + +Automatic checks fetch each product's public website using the shared SSRF-protected +fetcher. Qualification requires a concrete development use case, specific technologies, +and quotations found in the fetched page. Uncertain products remain unqualified; irrelevant +products are excluded from matching. Admins can retry checks or exclude a product without +editing source data. Exclusions survive subsequent syncs. +Retrying checks cancels any queued or running evaluation for the previous product revision; +successful requalification queues current matching results automatically. + +Qualified products receive private AI evaluations against up to 15 published tutorials +and five negative controls. Production placement matching continuously prepares product +and article/topic relevance separately, refreshing changed product and article inputs. +Reader delivery uses prepared matches and rechecks current commercial and qualification +settings. All product placements display **Sponsored** across the web reader and both extensions. +The API's `sponsored` flag records campaign backing for allocation and reporting; it must not +control disclosure or dismissal. Non-campaign, article, topic, and search placements require a relevant +match; sponsored defaults can fill unmatched slots in the unfiltered main feed. Trusted account +acceptance remains separate from AI qualification. Qualification expires +after 90 days. + +Migration `0024` creates the complete partnership schema after source-logo revision `0023`: +catalog, accounts, tiers, configurable connectors, promotion defaults, campaigns, prepared matching, +managed product logos/images, trusted acceptance, click destinations, account/campaign audiences, +native formats, permanent sponsored-article disclosure and product format/location restrictions. +It includes bounded enrollment jobs and indexed delivery history and retention. Run the normal +database upgrade before starting the API and workers. Downgrading to `0023` removes partnership +data; existing reader data and article/topic/source image jobs remain. Development databases with +superseded draft revision markers require repair based on their actual schema. +The scheduler dispatches discovery and product API syncs on `source-discovery` and product checks and +matching on `source-analysis`. AI must be enabled for checks; API syncing can continue +while AI is disabled. Jobs are leased, retried with backoff, and protected against stale +results. Import connections support public APIs or server-side bearer/API-key credentials. + +Connection, pause, manual sync, exclusion, and recheck actions emit structured admin API +logs after the change is committed. Scheduler and worker logs identify the provider, +operation, job, parent run, external product ID, attempt, and page, with page counts, qualification decisions, retries, +failures, cancellations, and completion timings. Sync/qualification worker logs use +`partner-pipeline`; article matching uses `partner-evaluation`. Both can be read through +`GET /v1/admin/jobs/{kind}/{job_id}/logs` with the existing admin authentication and log +retention policy. Connection action logs include affected job IDs for correlation. + +### Shared product identity + +The catalog displays one product with all of its platform listings. Product capabilities, +qualification, article matches, and exclusions belong to that shared identity. Each listing +retains its provider ID, platform URL, supplied description, pricing, attribution, availability, +and last sync time. Additional listings cannot overwrite the product's display information; +its original listing remains the explicit metadata source. Official-site evidence determines +verified capabilities. + +Identity matching normalizes full official product URLs and removes known tracking parameters. +Meaningful paths and query parameters are preserved; names and domains alone do not merge +products. Actual website redirects can verify HTTP-to-HTTPS and www aliases when the path and +query remain identical. Aliases are resolved before a redundant AI assessment, and valid +existing qualification is reused. Cross-domain or path-changing redirects do not establish +identity automatically. A listing whose website changes without verified identity remains +unresolved and cannot make its product eligible. Later syncs retry verification of its old +website and can resolve a matching safe redirect automatically. + +A platform sync withdraws only that platform's listings. A product becomes unavailable only +when no active, resolved listings remain. Pausing one platform does not cancel shared work +when another connected platform still supplies the product. Global exclusions survive new +listings and identity merges. Concurrent syncs serialize short catalog transactions and use +unique URL identities, avoiding duplicate products and qualification jobs. + +Nick Launches and configurable REST JSON connections share this identity model. Paid +campaigns and external ad network integrations do not determine product identity or +qualification. + ## Next.js 16.4 tooling The app uses Next.js 16.4 and React 19.3. The Rust React Compiler optimizes diff --git a/apps/admin/next.config.ts b/apps/admin/next.config.ts index 754d08b25..02a38331a 100644 --- a/apps/admin/next.config.ts +++ b/apps/admin/next.config.ts @@ -22,6 +22,20 @@ const config: NextConfig = { logging: { incomingRequests: false }, // Explicit LAN dev origins can be supplied without altering production trust. allowedDevOrigins: process.env.DEVFEED_ADMIN_DEV_ORIGINS?.split(",").filter(Boolean), + async redirects() { + return [ + { + source: "/partnerships/partners/:path*", + destination: "/partnerships/connections/:path*", + permanent: true, + }, + { + source: "/partner-tools", + destination: "/partnerships/connections", + permanent: true, + }, + ]; + }, async headers() { return [ { diff --git a/apps/admin/openapi.json b/apps/admin/openapi.json index 55fc0a852..ebc3f17d3 100644 --- a/apps/admin/openapi.json +++ b/apps/admin/openapi.json @@ -1,6 +1,312 @@ { "components": { "schemas": { + "AccountInput": { + "additionalProperties": false, + "properties": { + "ad_formats": { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "maxItems": 3, + "title": "Ad Formats", + "type": "array" + }, + "approval_mode": { + "default": "qualified", + "enum": [ + "qualified", + "trusted" + ], + "title": "Approval Mode", + "type": "string" + }, + "audience": { + "$ref": "#/components/schemas/AudienceInput" + }, + "auto_include_products": { + "default": true, + "title": "Auto Include Products", + "type": "boolean" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "organic_enabled": { + "default": true, + "title": "Organic Enabled", + "type": "boolean" + }, + "outbound_destination": { + "default": "product_url", + "enum": [ + "product_url", + "listing_url" + ], + "title": "Outbound Destination", + "type": "string" + }, + "promotion_kind": { + "anyOf": [ + { + "enum": [ + "product", + "ad" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Promotion Kind" + }, + "promotion_surfaces": { + "items": { + "enum": [ + "article", + "topic", + "feed" + ], + "type": "string" + }, + "maxItems": 3, + "minItems": 1, + "title": "Promotion Surfaces", + "type": "array" + }, + "status": { + "default": "active", + "enum": [ + "active", + "paused" + ], + "title": "Status", + "type": "string" + }, + "tier": { + "enum": [ + "bronze", + "silver", + "gold", + "platinum", + "diamond" + ], + "title": "Tier", + "type": "string" + } + }, + "required": [ + "name", + "tier" + ], + "title": "AccountInput", + "type": "object" + }, + "AccountOut": { + "additionalProperties": false, + "properties": { + "ad_formats": { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "maxItems": 3, + "title": "Ad Formats", + "type": "array" + }, + "advanced_targeting": { + "title": "Advanced Targeting", + "type": "boolean" + }, + "allowed_ad_formats": { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "title": "Allowed Ad Formats", + "type": "array" + }, + "allowed_promotion_kinds": { + "items": { + "enum": [ + "product", + "ad" + ], + "type": "string" + }, + "title": "Allowed Promotion Kinds", + "type": "array" + }, + "approval_mode": { + "default": "qualified", + "enum": [ + "qualified", + "trusted" + ], + "title": "Approval Mode", + "type": "string" + }, + "audience": { + "$ref": "#/components/schemas/AudienceInput" + }, + "auto_include_products": { + "default": true, + "title": "Auto Include Products", + "type": "boolean" + }, + "benefits": { + "items": { + "type": "string" + }, + "title": "Benefits", + "type": "array" + }, + "effective_promotion_kind": { + "enum": [ + "product", + "ad" + ], + "title": "Effective Promotion Kind", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "organic_enabled": { + "default": true, + "title": "Organic Enabled", + "type": "boolean" + }, + "outbound_destination": { + "default": "product_url", + "enum": [ + "product_url", + "listing_url" + ], + "title": "Outbound Destination", + "type": "string" + }, + "promotion_kind": { + "anyOf": [ + { + "enum": [ + "product", + "ad" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Promotion Kind" + }, + "promotion_kind_source": { + "enum": [ + "tier", + "account" + ], + "title": "Promotion Kind Source", + "type": "string" + }, + "promotion_surfaces": { + "items": { + "enum": [ + "article", + "topic", + "feed" + ], + "type": "string" + }, + "maxItems": 3, + "minItems": 1, + "title": "Promotion Surfaces", + "type": "array" + }, + "status": { + "default": "active", + "enum": [ + "active", + "paused" + ], + "title": "Status", + "type": "string" + }, + "tier": { + "enum": [ + "bronze", + "silver", + "gold", + "platinum", + "diamond" + ], + "title": "Tier", + "type": "string" + } + }, + "required": [ + "name", + "tier", + "id", + "benefits", + "allowed_promotion_kinds", + "effective_promotion_kind", + "promotion_kind_source", + "advanced_targeting", + "allowed_ad_formats" + ], + "title": "AccountOut", + "type": "object" + }, + "AccountsOut": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/AccountOut" + }, + "title": "Items", + "type": "array" + }, + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total" + ], + "title": "AccountsOut", + "type": "object" + }, "AdminArticleCreate": { "additionalProperties": false, "properties": { @@ -263,7 +569,7 @@ }, "topics": { "items": { - "$ref": "#/components/schemas/ArticleTopicOut" + "$ref": "#/components/schemas/devfeed_admin_api__articles__ArticleTopicOut" }, "title": "Topics", "type": "array" @@ -589,6 +895,8 @@ "enum": [ "ingestion", "source-discovery", + "partner-evaluation", + "partner-pipeline", "article-enrichment", "images", "source-enrichment", @@ -599,6 +907,18 @@ "title": "Kind", "type": "string" }, + "product_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Id" + }, "proposal_id": { "anyOf": [ { @@ -1413,6 +1733,13 @@ "notification_preferences": { "$ref": "#/components/schemas/NotificationSettings" }, + "partner_accounts": { + "items": { + "$ref": "#/components/schemas/AdminUserPartnerAccount" + }, + "title": "Partner Accounts", + "type": "array" + }, "profile_about": { "anyOf": [ { @@ -1817,6 +2144,25 @@ "title": "AdminUserOut", "type": "object" }, + "AdminUserPartnerAccount": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "title": "AdminUserPartnerAccount", + "type": "object" + }, "AdminUserRead": { "properties": { "article_id": { @@ -2381,13 +2727,36 @@ "title": "ArticleEnrichmentJobOut", "type": "object" }, - "ArticleReviewOut": { + "ArticleOriginOut": { "properties": { - "action": { - "title": "Action", + "original_url": { + "title": "Original Url", "type": "string" }, - "actor": { + "source_id": { + "format": "uuid", + "title": "Source Id", + "type": "string" + }, + "source_metadata": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Source Metadata", + "type": "object" + } + }, + "required": [ + "source_id", + "original_url", + "source_metadata" + ], + "title": "ArticleOriginOut", + "type": "object" + }, + "ArticleOut": { + "properties": { + "ai_description": { "anyOf": [ { "type": "string" @@ -2396,31 +2765,20 @@ "type": "null" } ], - "title": "Actor" - }, - "article_id": { - "format": "uuid", - "title": "Article Id", - "type": "string" + "title": "Ai Description" }, - "automation": { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "title": "Automation", - "type": "object" - }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" + "ai_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ai Summary" }, - "note": { + "author": { "anyOf": [ { "type": "string" @@ -2429,302 +2787,222 @@ "type": "null" } ], - "title": "Note" + "title": "Author" }, - "revision": { - "title": "Revision", - "type": "integer" - } - }, - "required": [ - "id", - "article_id", - "action", - "actor", - "note", - "revision", - "created_at" - ], - "title": "ArticleReviewOut", - "type": "object" - }, - "ArticleTopicOut": { - "properties": { - "evidence": { - "title": "Evidence", + "canonical_url": { + "title": "Canonical Url", "type": "string" }, - "name": { - "title": "Name", - "type": "string" + "content_format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content Format" }, - "origin": { - "title": "Origin", + "content_type": { + "title": "Content Type", "type": "string" }, - "relevance": { - "title": "Relevance", - "type": "number" - }, - "role": { - "title": "Role", + "discovered_at": { + "format": "date-time", + "title": "Discovered At", "type": "string" }, - "status": { - "title": "Status", + "feed_at": { + "format": "date-time", + "title": "Feed At", "type": "string" }, - "topic_id": { + "id": { "format": "uuid", - "title": "Topic Id", + "title": "Id", "type": "string" - } - }, - "required": [ - "topic_id", - "name", - "status", - "role", - "relevance", - "evidence", - "origin" - ], - "title": "ArticleTopicOut", - "type": "object" - }, - "AuthConfig": { - "properties": { - "enabled": { - "title": "Enabled", - "type": "boolean" }, - "providers": { - "items": { - "enum": [ - "github", - "google" - ], - "type": "string" - }, - "title": "Providers", - "type": "array" - } - }, - "required": [ - "enabled", - "providers" - ], - "title": "AuthConfig", - "type": "object" - }, - "AutomationBlocker": { - "properties": { - "action": { + "image_url": { "anyOf": [ { - "enum": [ - "enrich", - "analyze", - "evaluate" - ], "type": "string" }, { "type": "null" } ], - "title": "Action" - }, - "code": { - "title": "Code", - "type": "string" - }, - "count": { - "title": "Count", - "type": "integer" - }, - "label": { - "title": "Label", - "type": "string" + "title": "Image Url" }, - "targets": { + "image_variants": { "items": { - "$ref": "#/components/schemas/RecoveryTarget" + "$ref": "#/components/schemas/ImageVariant" }, - "title": "Targets", + "title": "Image Variants", "type": "array" - } - }, - "required": [ - "code", - "label", - "count", - "targets" - ], - "title": "AutomationBlocker", - "type": "object" - }, - "AutomationOverview": { - "properties": { - "analysis_duration_ms": { - "title": "Analysis Duration Ms", - "type": "integer" }, - "analysis_tokens": { - "title": "Analysis Tokens", - "type": "integer" + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Language of article text, from inference or operator classification; null for unresolved candidates. Source-declared language remains separate.", + "title": "Language" }, - "automatic_publication_percent": { + "metadata_source_type": { "anyOf": [ { - "type": "number" + "enum": [ + "publisher", + "aggregator", + "page" + ], + "type": "string" }, { "type": "null" } ], - "title": "Automatic Publication Percent" + "title": "Metadata Source Type" }, - "blockers": { + "origins": { "items": { - "$ref": "#/components/schemas/AutomationBlocker" + "$ref": "#/components/schemas/ArticleOriginOut" }, - "title": "Blockers", + "title": "Origins", "type": "array" }, - "full_automation": { - "title": "Full Automation", - "type": "boolean" - }, - "inference": { - "$ref": "#/components/schemas/InferenceCharts" - }, - "median_ingestion_to_publication_seconds": { + "published_at": { "anyOf": [ { - "type": "number" + "format": "date-time", + "type": "string" }, { "type": "null" } ], - "title": "Median Ingestion To Publication Seconds" + "title": "Published At" }, - "published_in_window": { - "title": "Published In Window", - "type": "integer" + "published_to_feed_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Published To Feed At" }, - "published_without_intervention": { - "title": "Published Without Intervention", - "type": "integer" + "slug": { + "title": "Slug", + "type": "string" }, - "throughput": { - "$ref": "#/components/schemas/PipelineThroughput" + "sources": { + "items": { + "$ref": "#/components/schemas/SourceRef" + }, + "title": "Sources", + "type": "array" }, - "token_activity": { + "sponsor_names": { "items": { - "$ref": "#/components/schemas/AnalysisTokenDay" + "type": "string" }, - "title": "Token Activity", + "title": "Sponsor Names", "type": "array" }, - "topic_decisions": { - "$ref": "#/components/schemas/TopicDecisionMetrics" + "summary": { + "title": "Summary", + "type": "string" }, - "usage_reported_runs": { - "title": "Usage Reported Runs", - "type": "integer" + "tags": { + "items": { + "type": "string" + }, + "title": "Tags", + "type": "array" }, - "usage_unreported_runs": { - "default": 0, - "title": "Usage Unreported Runs", - "type": "integer" - } - }, - "required": [ - "blockers", - "published_in_window", - "published_without_intervention", - "automatic_publication_percent", - "median_ingestion_to_publication_seconds", - "analysis_tokens", - "analysis_duration_ms", - "usage_reported_runs" - ], - "title": "AutomationOverview", - "type": "object" - }, - "CancelLogin": { - "properties": { - "login_id": { - "maxLength": 80, - "minLength": 1, - "pattern": "^[A-Za-z0-9-]+$", - "title": "Login Id", + "title": { + "title": "Title", "type": "string" + }, + "topics": { + "items": { + "$ref": "#/components/schemas/devfeed_core__schemas__ArticleTopicOut" + }, + "title": "Topics", + "type": "array" } }, "required": [ - "login_id" + "id", + "slug", + "canonical_url", + "title", + "summary", + "author", + "image_url", + "language", + "content_type", + "tags", + "published_at", + "feed_at", + "discovered_at", + "sources", + "origins" ], - "title": "CancelLogin", + "title": "ArticleOut", "type": "object" }, - "ClassifyArticle": { - "additionalProperties": false, + "ArticleReviewOut": { "properties": { - "actor": { - "title": "Actor", - "type": "null" - }, - "content_format": { - "enum": [ - "article", - "podcast", - "video", - "paper", - "discussion" - ], - "title": "Content Format", + "action": { + "title": "Action", "type": "string" }, - "content_type": { - "enum": [ - "article", - "news", - "tutorial", - "release", - "comparison", - "opinion" + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "title": "Content Type", - "type": "string" + "title": "Actor" }, - "developer_relevance": { - "enum": [ - "relevant", - "unrelated", - "uncertain" - ], - "title": "Developer Relevance", + "article_id": { + "format": "uuid", + "title": "Article Id", "type": "string" }, - "expected_revision": { - "minimum": 0.0, - "title": "Expected Revision", - "type": "integer" + "automation": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Automation", + "type": "object" }, - "language": { - "maxLength": 35, - "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$", - "title": "Language", + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, "note": { "anyOf": [ { - "maxLength": 1000, - "minLength": 1, "type": "string" }, { @@ -2733,798 +3011,1121 @@ ], "title": "Note" }, - "page_kind": { - "default": "uncertain", - "enum": [ - "article", - "non_article", - "uncertain" - ], - "title": "Page Kind", + "revision": { + "title": "Revision", + "type": "integer" + } + }, + "required": [ + "id", + "article_id", + "action", + "actor", + "note", + "revision", + "created_at" + ], + "title": "ArticleReviewOut", + "type": "object" + }, + "ArticleSnapshot": { + "properties": { + "content_type": { + "title": "Content Type", "type": "string" }, - "tags": { - "items": { - "$ref": "#/components/schemas/LabelSelection" - }, - "maxItems": 20, - "title": "Tags", - "type": "array" + "editorial_revision": { + "title": "Editorial Revision", + "type": "integer" }, - "topics": { - "items": { - "$ref": "#/components/schemas/TopicSelection" - }, - "maxItems": 12, - "title": "Topics", - "type": "array" + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "published": { + "title": "Published", + "type": "boolean" + }, + "text": { + "title": "Text", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" } }, "required": [ - "developer_relevance", - "language", + "id", + "title", + "text", "content_type", - "content_format", - "topics", - "tags", - "expected_revision" + "published", + "editorial_revision" ], - "title": "ClassifyArticle", + "title": "ArticleSnapshot", "type": "object" }, - "CodexQuota": { + "AssetInput": { "properties": { - "resets_at": { + "kind": { + "enum": [ + "product", + "ad" + ], + "title": "Kind", + "type": "string" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "product_id": { "anyOf": [ { - "format": "date-time", + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Resets At" - }, - "used_percent": { - "title": "Used Percent", - "type": "number" + "title": "Product Id" }, - "window_minutes": { - "title": "Window Minutes", - "type": "integer" + "status": { + "default": "draft", + "enum": [ + "draft", + "active", + "paused", + "ended" + ], + "title": "Status", + "type": "string" } }, "required": [ - "used_percent", - "window_minutes" + "name", + "kind" ], - "title": "CodexQuota", + "title": "AssetInput", "type": "object" }, - "CodexStatus": { + "AssetMetrics": { "properties": { - "checked_at": { + "ad_format": { + "default": "feed", + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "title": "Ad Format", + "type": "string" + }, + "ad_formats": { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "title": "Ad Formats", + "type": "array" + }, + "aggregated": { + "default": false, + "title": "Aggregated", + "type": "boolean" + }, + "article_id": { "anyOf": [ { - "format": "date-time", + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Checked At" + "title": "Article Id" }, - "email": { + "automatic": { + "default": false, + "title": "Automatic", + "type": "boolean" + }, + "campaign_id": { "anyOf": [ { + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Email" + "title": "Campaign Id" }, - "login": { + "clicks": { + "title": "Clicks", + "type": "integer" + }, + "ctr": { "anyOf": [ { - "$ref": "#/components/schemas/DeviceLogin" + "type": "number" }, { "type": "null" } - ] + ], + "title": "Ctr" }, - "message": { - "title": "Message", + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, - "model": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } + "impressions": { + "title": "Impressions", + "type": "integer" + }, + "kind": { + "enum": [ + "product", + "ad" ], - "title": "Model" + "title": "Kind", + "type": "string" }, - "plan": { + "measured_days": { + "title": "Measured Days", + "type": "integer" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "product_id": { "anyOf": [ { + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Plan" - }, - "quota": { - "items": { - "$ref": "#/components/schemas/CodexQuota" - }, - "title": "Quota", - "type": "array" + "title": "Product Id" }, - "state": { + "status": { + "default": "draft", "enum": [ - "disabled", - "checking", - "unavailable", - "signed_out", - "connected", - "limited", - "error" + "draft", + "active", + "paused", + "ended" ], - "title": "State", + "title": "Status", "type": "string" } }, "required": [ - "state", - "message" + "impressions", + "clicks", + "ctr", + "measured_days", + "name", + "kind", + "id" ], - "title": "CodexStatus", + "title": "AssetMetrics", "type": "object" }, - "DefaultSettings": { - "additionalProperties": false, + "AssetOut": { "properties": { - "landing_page": { - "default": "/", + "ad_format": { + "default": "feed", "enum": [ - "/", - "/content/articles", - "/taxonomy/topics", - "/jobs/analysis" + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" ], - "title": "Landing Page", + "title": "Ad Format", "type": "string" }, - "overview_days": { - "default": 30, - "enum": [ - 7, - 30 + "article_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } ], - "title": "Overview Days", - "type": "integer" + "title": "Article Id" }, - "page_size": { - "default": 25, - "enum": [ - 10, - 25, - 50, - 100 + "automatic": { + "default": false, + "title": "Automatic", + "type": "boolean" + }, + "campaign_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } ], - "title": "Page Size", - "type": "integer" + "title": "Campaign Id" }, - "refresh_seconds": { - "default": 10, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "kind": { "enum": [ - 0, - 5, - 10, - 15, - 30, - 60 + "product", + "ad" ], - "title": "Refresh Seconds", - "type": "integer" + "title": "Kind", + "type": "string" }, - "remember_columns": { - "default": true, - "title": "Remember Columns", - "type": "boolean" + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" }, - "remember_filters": { - "default": true, - "title": "Remember Filters", - "type": "boolean" + "product_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Id" }, - "remember_sort": { - "default": true, - "title": "Remember Sort", - "type": "boolean" + "status": { + "default": "draft", + "enum": [ + "draft", + "active", + "paused", + "ended" + ], + "title": "Status", + "type": "string" } }, - "title": "DefaultSettings", + "required": [ + "name", + "kind", + "id" + ], + "title": "AssetOut", "type": "object" }, - "DeleteImportedPublishers": { + "AudienceInput": { "additionalProperties": false, "properties": { - "ids": { + "experience_levels": { + "items": { + "enum": [ + "aspiring", + "entry_level", + "mid_level", + "senior" + ], + "type": "string" + }, + "maxItems": 4, + "title": "Experience Levels", + "type": "array" + }, + "language_topic_ids": { "items": { "format": "uuid", "type": "string" }, "maxItems": 100, - "minItems": 1, - "title": "Ids", + "title": "Language Topic Ids", + "type": "array" + }, + "tool_topic_ids": { + "items": { + "format": "uuid", + "type": "string" + }, + "maxItems": 100, + "title": "Tool Topic Ids", "type": "array" } }, + "title": "AudienceInput", + "type": "object" + }, + "AudienceTopicOut": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, "required": [ - "ids" + "id", + "name", + "slug", + "kind", + "status" ], - "title": "DeleteImportedPublishers", + "title": "AudienceTopicOut", "type": "object" }, - "DeletedImportedPublishers": { + "AudienceTopicsOut": { "properties": { - "deleted": { - "title": "Deleted", + "items": { + "items": { + "$ref": "#/components/schemas/AudienceTopicOut" + }, + "title": "Items", + "type": "array" + }, + "total": { + "title": "Total", "type": "integer" } }, "required": [ - "deleted" + "items", + "total" ], - "title": "DeletedImportedPublishers", + "title": "AudienceTopicsOut", "type": "object" }, - "DevCardSettings": { - "additionalProperties": false, - "description": "Content a user chooses to show on their public Dev Card.", + "AuthConfig": { "properties": { - "accent": { - "default": "default", - "enum": [ - "default", - "teal", - "violet", - "blue", - "amber", - "rose" - ], - "title": "Accent", - "type": "string" - }, - "motion": { - "default": "animated", - "enum": [ - "static", - "animated" - ], - "title": "Motion", - "type": "string" + "enabled": { + "title": "Enabled", + "type": "boolean" }, - "stats": { + "providers": { "items": { "enum": [ - "current_streak", - "longest_streak", - "total_reading_days" + "github", + "google" ], "type": "string" }, - "maxItems": 3, - "title": "Stats", + "title": "Providers", "type": "array" - }, - "technologies": { + } + }, + "required": [ + "enabled", + "providers" + ], + "title": "AuthConfig", + "type": "object" + }, + "AutomationBlocker": { + "properties": { + "action": { "anyOf": [ { - "items": { - "format": "uuid", - "type": "string" - }, - "type": "array" + "enum": [ + "enrich", + "analyze", + "evaluate" + ], + "type": "string" }, { "type": "null" } ], - "title": "Technologies" + "title": "Action" }, - "theme": { - "default": "classic", - "enum": [ - "classic", - "terminal", - "aurora", - "minimal" - ], - "title": "Theme", + "code": { + "title": "Code", + "type": "string" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "label": { + "title": "Label", "type": "string" + }, + "targets": { + "items": { + "$ref": "#/components/schemas/RecoveryTarget" + }, + "title": "Targets", + "type": "array" } }, - "title": "DevCardSettings", + "required": [ + "code", + "label", + "count", + "targets" + ], + "title": "AutomationBlocker", "type": "object" }, - "DeviceLogin": { + "AutomationOverview": { "properties": { - "expires_at": { - "format": "date-time", - "title": "Expires At", - "type": "string" + "analysis_duration_ms": { + "title": "Analysis Duration Ms", + "type": "integer" }, - "login_id": { - "title": "Login Id", - "type": "string" + "analysis_tokens": { + "title": "Analysis Tokens", + "type": "integer" }, - "message": { + "automatic_publication_percent": { "anyOf": [ { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Message" + "title": "Automatic Publication Percent" }, - "status": { - "default": "pending", - "enum": [ - "pending", - "completed", - "failed", - "cancelled", - "expired" - ], - "title": "Status", - "type": "string" + "blockers": { + "items": { + "$ref": "#/components/schemas/AutomationBlocker" + }, + "title": "Blockers", + "type": "array" }, - "user_code": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "User Code" + "full_automation": { + "title": "Full Automation", + "type": "boolean" }, - "verification_url": { + "inference": { + "$ref": "#/components/schemas/InferenceCharts" + }, + "median_ingestion_to_publication_seconds": { "anyOf": [ { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Verification Url" - } - }, - "required": [ - "login_id", - "expires_at" - ], - "title": "DeviceLogin", - "type": "object" - }, - "EntryQuality": { - "additionalProperties": false, - "properties": { - "depth": { - "enum": [ - "substantial", - "light", - "uncertain" - ], - "title": "Depth", - "type": "string" + "title": "Median Ingestion To Publication Seconds" }, - "index": { - "maximum": 9.0, - "minimum": 0.0, - "title": "Index", + "published_in_window": { + "title": "Published In Window", "type": "integer" }, - "promotion": { - "enum": [ - "primarily_promotional", - "editorial", - "uncertain" - ], - "title": "Promotion", - "type": "string" + "published_without_intervention": { + "title": "Published Without Intervention", + "type": "integer" }, - "quote": { - "maxLength": 500, - "title": "Quote", - "type": "string" + "throughput": { + "$ref": "#/components/schemas/PipelineThroughput" }, - "relevance": { - "enum": [ - "relevant", - "unrelated", - "uncertain" - ], - "title": "Relevance", - "type": "string" + "token_activity": { + "items": { + "$ref": "#/components/schemas/AnalysisTokenDay" + }, + "title": "Token Activity", + "type": "array" + }, + "topic_decisions": { + "$ref": "#/components/schemas/TopicDecisionMetrics" + }, + "usage_reported_runs": { + "title": "Usage Reported Runs", + "type": "integer" + }, + "usage_unreported_runs": { + "default": 0, + "title": "Usage Unreported Runs", + "type": "integer" } }, "required": [ - "index", - "relevance", - "depth", - "promotion", - "quote" + "blockers", + "published_in_window", + "published_without_intervention", + "automatic_publication_percent", + "median_ingestion_to_publication_seconds", + "analysis_tokens", + "analysis_duration_ms", + "usage_reported_runs" ], - "title": "EntryQuality", + "title": "AutomationOverview", "type": "object" }, - "ErrorDetail": { + "Body_adminPartnerProductAdImageUpload": { "properties": { - "code": { - "title": "Code", - "type": "string" - }, - "message": { - "title": "Message", + "file": { + "contentMediaType": "application/octet-stream", + "description": "JPEG, PNG or WebP, up to 5 MB", + "title": "File", "type": "string" } }, "required": [ - "code", - "message" + "file" ], - "title": "ErrorDetail", + "title": "Body_adminPartnerProductAdImageUpload", "type": "object" }, - "ErrorResponse": { + "CampaignInput": { + "additionalProperties": false, "properties": { - "detail": { + "ad_format": { + "default": "feed", + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "title": "Ad Format", + "type": "string" + }, + "article_id": { "anyOf": [ { + "format": "uuid", "type": "string" }, { - "$ref": "#/components/schemas/ErrorDetail" + "type": "null" + } + ], + "title": "Article Id" + }, + "audience": { + "anyOf": [ + { + "$ref": "#/components/schemas/AudienceInput" }, { - "items": { - "$ref": "#/components/schemas/ValidationIssue" - }, - "type": "array" + "type": "null" + } + ] + }, + "daily_impression_limit": { + "anyOf": [ + { + "maximum": 9.223372036854776e+18, + "minimum": 1.0, + "type": "integer" + }, + { + "type": "null" } ], - "title": "Detail" - } - }, - "required": [ - "detail" - ], - "title": "ErrorResponse", - "type": "object" - }, - "FeedSettings": { - "additionalProperties": false, - "properties": { - "content_types": { + "title": "Daily Impression Limit" + }, + "ends_at": { + "format": "date-time", + "title": "Ends At", + "type": "string" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "priority": { + "default": 50, + "maximum": 100.0, + "minimum": 1.0, + "title": "Priority", + "type": "integer" + }, + "product_ids": { "items": { - "enum": [ - "article", - "news", - "tutorial", - "release", - "comparison", - "opinion" - ], + "format": "uuid", "type": "string" }, - "maxItems": 6, - "minItems": 1, - "title": "Content Types", + "maxItems": 500, + "title": "Product Ids", "type": "array" }, - "languages": { + "starts_at": { + "format": "date-time", + "title": "Starts At", + "type": "string" + }, + "status": { + "default": "draft", + "enum": [ + "draft", + "active", + "paused", + "ended" + ], + "title": "Status", + "type": "string" + }, + "surfaces": { "items": { "enum": [ - "en", - "af", - "sq", - "ar", - "hy", - "az", - "eu", - "be", - "bn", - "nb", - "bs", - "bg", - "ca", - "zh", - "hr", - "cs", - "da", - "nl", - "eo", - "et", - "fi", - "fr", - "lg", - "ka", - "de", - "el", - "gu", - "he", - "hi", - "hu", - "is", - "id", - "ga", - "it", - "ja", - "kk", - "ko", - "la", - "lv", - "lt", - "mk", - "ms", - "mi", - "mr", - "mn", - "nn", - "fa", - "pl", - "pt", - "pa", - "ro", - "ru", - "sr", - "sn", - "sk", - "sl", - "so", - "st", - "es", - "sw", - "sv", - "tl", - "ta", - "te", - "th", - "ts", - "tn", - "tr", - "uk", - "ur", - "vi", - "cy", - "xh", - "yo", - "zu" + "article", + "topic", + "feed" ], "type": "string" }, - "maxItems": 75, + "maxItems": 3, "minItems": 1, - "title": "Languages", + "title": "Surfaces", "type": "array" }, - "view": { - "default": "cards", + "targeting": { + "default": "all", "enum": [ - "cards", - "compact" + "all", + "selected", + "filters" ], - "title": "View", + "title": "Targeting", "type": "string" + }, + "technologies": { + "items": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "maxItems": 100, + "title": "Technologies", + "type": "array" + }, + "topic_ids": { + "items": { + "format": "uuid", + "type": "string" + }, + "maxItems": 100, + "title": "Topic Ids", + "type": "array" } }, - "title": "FeedSettings", + "required": [ + "name", + "starts_at", + "ends_at" + ], + "title": "CampaignInput", "type": "object" }, - "GitHubPull": { + "CampaignOut": { "additionalProperties": false, "properties": { - "offset": { - "default": 0, - "maximum": 10000.0, - "minimum": 0.0, - "title": "Offset", - "type": "integer" + "account_id": { + "format": "uuid", + "title": "Account Id", + "type": "string" }, - "revision": { + "ad_format": { + "default": "feed", + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "title": "Ad Format", + "type": "string" + }, + "article_id": { "anyOf": [ { - "pattern": "^[a-f0-9]{40}$", + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Revision" - } - }, - "title": "GitHubPull", - "type": "object" - }, - "GitHubPullResult": { - "additionalProperties": false, - "properties": { - "created": { - "title": "Created", + "title": "Article Id" + }, + "audience": { + "anyOf": [ + { + "$ref": "#/components/schemas/AudienceInput" + }, + { + "type": "null" + } + ] + }, + "clicks": { + "default": 0, + "title": "Clicks", "type": "integer" }, - "issues": { - "items": { - "type": "string" - }, - "title": "Issues", - "type": "array" + "ctr": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Ctr" }, - "next_offset": { + "daily_impression_limit": { "anyOf": [ { + "maximum": 9.223372036854776e+18, + "minimum": 1.0, "type": "integer" }, { "type": "null" } ], - "title": "Next Offset" + "title": "Daily Impression Limit" }, - "processed": { - "title": "Processed", - "type": "integer" + "ends_at": { + "format": "date-time", + "title": "Ends At", + "type": "string" }, - "revision": { - "pattern": "^[a-f0-9]{40}$", - "title": "Revision", + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, - "skipped": { - "title": "Skipped", + "impressions": { + "default": 0, + "title": "Impressions", "type": "integer" }, - "total": { - "title": "Total", + "managed": { + "default": false, + "title": "Managed", + "type": "boolean" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "priority": { + "default": 50, + "maximum": 100.0, + "minimum": 1.0, + "title": "Priority", "type": "integer" - } - }, - "required": [ - "revision", - "total", - "processed", - "created", - "skipped", - "issues", - "next_offset" - ], - "title": "GitHubPullResult", - "type": "object" - }, - "HealthResponse": { - "properties": { + }, + "product_ids": { + "items": { + "format": "uuid", + "type": "string" + }, + "maxItems": 500, + "title": "Product Ids", + "type": "array" + }, + "starts_at": { + "format": "date-time", + "title": "Starts At", + "type": "string" + }, "status": { - "const": "ok", + "default": "draft", + "enum": [ + "draft", + "active", + "paused", + "ended" + ], "title": "Status", "type": "string" - } - }, - "required": [ - "status" - ], - "title": "HealthResponse", + }, + "surfaces": { + "items": { + "enum": [ + "article", + "topic", + "feed" + ], + "type": "string" + }, + "maxItems": 3, + "minItems": 1, + "title": "Surfaces", + "type": "array" + }, + "targeting": { + "default": "all", + "enum": [ + "all", + "selected", + "filters" + ], + "title": "Targeting", + "type": "string" + }, + "technologies": { + "items": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "maxItems": 100, + "title": "Technologies", + "type": "array" + }, + "topic_ids": { + "items": { + "format": "uuid", + "type": "string" + }, + "maxItems": 100, + "title": "Topic Ids", + "type": "array" + } + }, + "required": [ + "name", + "starts_at", + "ends_at", + "id", + "account_id" + ], + "title": "CampaignOut", "type": "object" }, - "ImageVariant": { + "CampaignsOut": { "properties": { - "url": { - "title": "Url", - "type": "string" + "items": { + "items": { + "$ref": "#/components/schemas/CampaignOut" + }, + "title": "Items", + "type": "array" }, - "width": { - "title": "Width", + "total": { + "title": "Total", "type": "integer" } }, "required": [ - "url", - "width" + "items", + "total" ], - "title": "ImageVariant", + "title": "CampaignsOut", "type": "object" }, - "ImportAction": { + "CancelLogin": { + "properties": { + "login_id": { + "maxLength": 80, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "title": "Login Id", + "type": "string" + } + }, + "required": [ + "login_id" + ], + "title": "CancelLogin", + "type": "object" + }, + "ClassifyArticle": { "additionalProperties": false, "properties": { - "action": { + "actor": { + "title": "Actor", + "type": "null" + }, + "content_format": { "enum": [ - "discover", - "assess", - "select", - "approve", - "reject" + "article", + "podcast", + "video", + "paper", + "discussion" ], - "title": "Action", + "title": "Content Format", "type": "string" }, - "feed_id": { + "content_type": { + "enum": [ + "article", + "news", + "tutorial", + "release", + "comparison", + "opinion" + ], + "title": "Content Type", + "type": "string" + }, + "developer_relevance": { + "enum": [ + "relevant", + "unrelated", + "uncertain" + ], + "title": "Developer Relevance", + "type": "string" + }, + "expected_revision": { + "minimum": 0.0, + "title": "Expected Revision", + "type": "integer" + }, + "language": { + "maxLength": 35, + "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$", + "title": "Language", + "type": "string" + }, + "note": { "anyOf": [ { - "format": "uuid", + "maxLength": 1000, + "minLength": 1, "type": "string" }, { "type": "null" } ], - "title": "Feed Id" + "title": "Note" }, - "note": { + "page_kind": { + "default": "uncertain", + "enum": [ + "article", + "non_article", + "uncertain" + ], + "title": "Page Kind", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/LabelSelection" + }, + "maxItems": 20, + "title": "Tags", + "type": "array" + }, + "topics": { + "items": { + "$ref": "#/components/schemas/TopicSelection" + }, + "maxItems": 12, + "title": "Topics", + "type": "array" + } + }, + "required": [ + "developer_relevance", + "language", + "content_type", + "content_format", + "topics", + "tags", + "expected_revision" + ], + "title": "ClassifyArticle", + "type": "object" + }, + "CodexQuota": { + "properties": { + "resets_at": { "anyOf": [ { - "maxLength": 1000, - "minLength": 1, + "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Note" + "title": "Resets At" + }, + "used_percent": { + "title": "Used Percent", + "type": "number" + }, + "window_minutes": { + "title": "Window Minutes", + "type": "integer" } }, "required": [ - "action" + "used_percent", + "window_minutes" ], - "title": "ImportAction", + "title": "CodexQuota", "type": "object" }, - "ImportAssessmentEvidence": { + "CodexStatus": { "properties": { - "actor": { + "checked_at": { "anyOf": [ { + "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Actor" - }, - "components": { - "additionalProperties": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - }, - "title": "Components", - "type": "object" + "title": "Checked At" }, - "decision": { + "email": { "anyOf": [ { "type": "string" @@ -3533,19 +4134,23 @@ "type": "null" } ], - "title": "Decision" + "title": "Email" }, - "model_output": { + "login": { "anyOf": [ { - "$ref": "#/components/schemas/Quality" + "$ref": "#/components/schemas/DeviceLogin" }, { "type": "null" } ] }, - "reason": { + "message": { + "title": "Message", + "type": "string" + }, + "model": { "anyOf": [ { "type": "string" @@ -3554,9 +4159,9 @@ "type": "null" } ], - "title": "Reason" + "title": "Model" }, - "recommendation": { + "plan": { "anyOf": [ { "type": "string" @@ -3565,120 +4170,59 @@ "type": "null" } ], - "title": "Recommendation" + "title": "Plan" }, - "sample": { + "quota": { "items": { - "$ref": "#/components/schemas/ImportSample" + "$ref": "#/components/schemas/CodexQuota" }, - "title": "Sample", + "title": "Quota", "type": "array" }, - "score": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } + "state": { + "enum": [ + "disabled", + "checking", + "unavailable", + "signed_out", + "connected", + "limited", + "error" ], - "title": "Score" + "title": "State", + "type": "string" } }, - "title": "ImportAssessmentEvidence", + "required": [ + "state", + "message" + ], + "title": "CodexStatus", "type": "object" }, - "ImportAssessmentOut": { + "ConnectionAction": { + "additionalProperties": false, "properties": { - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" - }, - "evidence": { - "$ref": "#/components/schemas/ImportAssessmentEvidence" - }, - "feed_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } + "action": { + "enum": [ + "connect", + "pause", + "sync" ], - "title": "Feed Id" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "version": { - "title": "Version", + "title": "Action", "type": "string" } }, "required": [ - "id", - "version", - "feed_id", - "created_at", - "evidence" + "action" ], - "title": "ImportAssessmentOut", + "title": "ConnectionAction", "type": "object" }, - "ImportCandidateDetail": { + "ConnectionCreate": { + "additionalProperties": false, "properties": { - "approval_status": { - "title": "Approval Status", - "type": "string" - }, - "assessments": { - "items": { - "$ref": "#/components/schemas/ImportAssessmentOut" - }, - "title": "Assessments", - "type": "array" - }, - "feeds": { - "items": { - "$ref": "#/components/schemas/ImportFeedOut" - }, - "title": "Feeds", - "type": "array" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "identity_url": { - "title": "Identity Url", - "type": "string" - }, - "jobs": { - "items": { - "$ref": "#/components/schemas/ImportJobOut" - }, - "title": "Jobs", - "type": "array" - }, - "name": { - "title": "Name", - "type": "string" - }, - "provenance": { - "items": { - "$ref": "#/components/schemas/ImportProvenanceOut" - }, - "title": "Provenance", - "type": "array" - }, - "selected_feed_id": { + "account_id": { "anyOf": [ { "format": "uuid", @@ -3688,67 +4232,59 @@ "type": "null" } ], - "title": "Selected Feed Id" + "title": "Account Id" }, - "source_id": { + "connector": { "anyOf": [ { - "format": "uuid", + "$ref": "#/components/schemas/ConnectorConfig" + }, + { + "type": "null" + } + ] + }, + "enabled": { + "default": true, + "title": "Enabled", + "type": "boolean" + }, + "name": { + "anyOf": [ + { + "maxLength": 200, + "minLength": 1, "type": "string" }, { "type": "null" } ], - "title": "Source Id" + "title": "Name" }, - "status": { - "title": "Status", + "provider": { + "maxLength": 200, + "minLength": 1, + "title": "Provider", "type": "string" }, - "updated_at": { - "format": "date-time", - "title": "Updated At", - "type": "string" + "sync_interval_minutes": { + "default": 360, + "maximum": 10080.0, + "minimum": 1.0, + "title": "Sync Interval Minutes", + "type": "integer" } }, "required": [ - "id", - "name", - "identity_url", - "status", - "approval_status", - "selected_feed_id", - "source_id", - "updated_at", - "feeds", - "assessments", - "jobs", - "provenance" + "provider" ], - "title": "ImportCandidateDetail", + "title": "ConnectionCreate", "type": "object" }, - "ImportCandidateOut": { + "ConnectionOut": { "properties": { - "approval_status": { - "title": "Approval Status", - "type": "string" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "identity_url": { - "title": "Identity Url", - "type": "string" - }, - "name": { - "title": "Name", - "type": "string" - }, - "selected_feed_id": { + "account_id": { "anyOf": [ { "format": "uuid", @@ -3758,105 +4294,55 @@ "type": "null" } ], - "title": "Selected Feed Id" + "title": "Account Id" }, - "source_id": { + "ai_enabled": { + "title": "Ai Enabled", + "type": "boolean" + }, + "api_url": { + "title": "Api Url", + "type": "string" + }, + "approval_mode": { "anyOf": [ { - "format": "uuid", + "enum": [ + "qualified", + "trusted" + ], "type": "string" }, { "type": "null" } ], - "title": "Source Id" - }, - "status": { - "title": "Status", - "type": "string" - }, - "updated_at": { - "format": "date-time", - "title": "Updated At", - "type": "string" - } - }, - "required": [ - "id", - "name", - "identity_url", - "status", - "approval_status", - "selected_feed_id", - "source_id", - "updated_at" - ], - "title": "ImportCandidateOut", - "type": "object" - }, - "ImportCandidatePage": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/ImportCandidateOut" - }, - "title": "Items", - "type": "array" + "title": "Approval Mode" }, - "limit": { - "title": "Limit", + "checking": { + "title": "Checking", "type": "integer" }, - "offset": { - "title": "Offset", + "commercial_approved": { + "default": 0, + "title": "Commercial Approved", "type": "integer" }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "offset", - "limit" - ], - "title": "ImportCandidatePage", - "type": "object" - }, - "ImportFeedEvidence": { - "properties": { - "publisher_ratio": { + "connector": { "anyOf": [ { - "type": "number" + "$ref": "#/components/schemas/ConnectorConfig" }, { "type": "null" } - ], - "title": "Publisher Ratio" - }, - "publisher_type": { - "default": "unknown", - "title": "Publisher Type", - "type": "string" + ] }, - "recent": { - "default": false, - "title": "Recent", + "enabled": { + "title": "Enabled", "type": "boolean" }, - "sample": { - "items": { - "$ref": "#/components/schemas/ImportSample" - }, - "title": "Sample", - "type": "array" - }, - "title": { + "error": { "anyOf": [ { "type": "string" @@ -3865,454 +4351,665 @@ "type": "null" } ], - "title": "Title" + "title": "Error" }, - "usable_entries": { - "default": 0, - "title": "Usable Entries", + "excluded": { + "title": "Excluded", "type": "integer" - } - }, - "title": "ImportFeedEvidence", - "type": "object" - }, - "ImportFeedOut": { - "properties": { - "checked_at": { - "format": "date-time", - "title": "Checked At", + }, + "last_sync_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Sync At" + }, + "name": { + "title": "Name", "type": "string" }, - "evidence": { - "$ref": "#/components/schemas/ImportFeedEvidence" + "needs_attention": { + "title": "Needs Attention", + "type": "integer" }, - "id": { - "format": "uuid", - "title": "Id", + "next_sync_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Sync At" + }, + "partnership_type": { + "const": "launch_platform", + "default": "launch_platform", + "title": "Partnership Type", "type": "string" }, - "url": { - "title": "Url", + "products": { + "title": "Products", + "type": "integer" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "qualified": { + "title": "Qualified", + "type": "integer" + }, + "revision": { + "title": "Revision", + "type": "integer" + }, + "state": { + "enum": [ + "disconnected", + "idle", + "syncing", + "error" + ], + "title": "State", "type": "string" + }, + "sync_interval_minutes": { + "title": "Sync Interval Minutes", + "type": "integer" } }, "required": [ - "id", - "url", - "evidence", - "checked_at" - ], - "title": "ImportFeedOut", + "sync_interval_minutes", + "revision", + "provider", + "name", + "api_url", + "enabled", + "state", + "last_sync_at", + "next_sync_at", + "error", + "products", + "qualified", + "checking", + "needs_attention", + "excluded", + "ai_enabled" + ], + "title": "ConnectionOut", "type": "object" }, - "ImportJobOut": { + "ConnectionSettings": { + "additionalProperties": false, "properties": { - "attempts": { - "title": "Attempts", - "type": "integer" - }, - "available_at": { - "format": "date-time", - "title": "Available At", - "type": "string" - }, - "error": { + "account_id": { "anyOf": [ { + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Error" + "title": "Account Id" }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" + "connector": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConnectorConfig" + }, + { + "type": "null" + } + ] }, - "lease_until": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "expected_revision": { + "minimum": 1.0, + "title": "Expected Revision", + "type": "integer" + }, + "name": { "anyOf": [ { - "format": "date-time", + "maxLength": 200, + "minLength": 1, "type": "string" }, { "type": "null" } ], - "title": "Lease Until" - }, - "stage": { - "title": "Stage", - "type": "string" + "title": "Name" }, - "status": { - "title": "Status", - "type": "string" + "sync_interval_minutes": { + "anyOf": [ + { + "maximum": 10080.0, + "minimum": 1.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sync Interval Minutes" } }, "required": [ - "id", - "stage", - "status", - "attempts", - "error", - "available_at", - "lease_until" + "enabled", + "expected_revision" ], - "title": "ImportJobOut", + "title": "ConnectionSettings", "type": "object" }, - "ImportProvenanceOut": { + "ConnectorAuth": { + "additionalProperties": false, "properties": { - "created_at": { - "format": "date-time", - "title": "Created At", + "header": { + "default": "X-API-Key", + "maxLength": 100, + "minLength": 1, + "pattern": "^[A-Za-z][A-Za-z0-9_-]*$", + "title": "Header", "type": "string" }, - "origin": { - "title": "Origin", + "mode": { + "default": "none", + "enum": [ + "none", + "bearer", + "api_key" + ], + "title": "Mode", + "type": "string" + }, + "secret_ref": { + "default": "", + "pattern": "^$|^DEVFEED_PARTNER_SECRET_[A-Z0-9_]{1,80}$", + "title": "Secret Ref", + "type": "string" + }, + "source": { + "default": "environment", + "enum": [ + "environment", + "connection" + ], + "title": "Source", "type": "string" } }, - "required": [ - "origin", - "created_at" - ], - "title": "ImportProvenanceOut", + "title": "ConnectorAuth", "type": "object" }, - "ImportSample": { + "ConnectorConfig": { + "additionalProperties": false, "properties": { - "published_at": { + "attribution": { + "default": "", + "maxLength": 300, + "title": "Attribution", + "type": "string" + }, + "auth": { + "$ref": "#/components/schemas/ConnectorAuth" + }, + "base_url": { + "title": "Base Url", + "type": "string" + }, + "detail_path": { "anyOf": [ { + "maxLength": 500, "type": "string" }, { "type": "null" } ], - "title": "Published At" + "title": "Detail Path" }, - "summary": { - "title": "Summary", + "detail_root": { + "default": "", + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "title": "Detail Root", "type": "string" }, - "title": { - "title": "Title", - "type": "string" + "fields": { + "$ref": "#/components/schemas/ConnectorFields" }, - "url": { - "title": "Url", - "type": "string" - } - }, - "required": [ - "title", - "summary", - "url" - ], - "title": "ImportSample", - "type": "object" - }, - "InferenceCharts": { - "properties": { - "activity": { + "filters": { "items": { - "$ref": "#/components/schemas/InferencePoint" + "$ref": "#/components/schemas/ConnectorFilter" }, - "title": "Activity", + "maxItems": 10, + "title": "Filters", "type": "array" }, - "first_recorded_at": { + "items_paths": { + "items": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "type": "string" + }, + "maxItems": 5, + "minItems": 1, + "title": "Items Paths", + "type": "array" + }, + "list_path": { + "default": "/products", + "maxLength": 500, + "minLength": 1, + "title": "List Path", + "type": "string" + }, + "listing_url_template": { "anyOf": [ { - "format": "date-time", + "maxLength": 500, "type": "string" }, { "type": "null" } ], - "title": "First Recorded At" + "title": "Listing Url Template" }, - "topic_activity": { - "items": { - "$ref": "#/components/schemas/TopicDecisionDay" - }, - "title": "Topic Activity", - "type": "array" - } - }, - "title": "InferenceCharts", - "type": "object" - }, - "InferencePoint": { - "properties": { - "cached_input_tokens": { - "title": "Cached Input Tokens", + "max_pages": { + "default": 1000, + "maximum": 1000.0, + "minimum": 1.0, + "title": "Max Pages", "type": "integer" }, - "calls": { - "title": "Calls", + "max_response_bytes": { + "default": 2000000, + "maximum": 5000000.0, + "minimum": 1024.0, + "title": "Max Response Bytes", "type": "integer" }, - "date": { - "title": "Date", - "type": "string" + "pagination": { + "$ref": "#/components/schemas/ConnectorPagination" }, - "duration_ms": { - "title": "Duration Ms", - "type": "integer" + "parameters": { + "maxProperties": 20, + "patternProperties": { + "^[A-Za-z][A-Za-z0-9_-]*$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 100, + "minLength": 1 + }, + "title": "Parameters", + "type": "object" }, - "effort": { - "title": "Effort", - "type": "string" + "platform_hosts": { + "items": { + "type": "string" + }, + "maxItems": 5, + "title": "Platform Hosts", + "type": "array" }, - "failed": { - "title": "Failed", + "requests_per_minute": { + "default": 60, + "maximum": 600.0, + "minimum": 1.0, + "title": "Requests Per Minute", "type": "integer" }, - "input_tokens": { - "title": "Input Tokens", + "timeout_seconds": { + "default": 15, + "maximum": 30.0, + "minimum": 1.0, + "title": "Timeout Seconds", "type": "integer" }, - "model": { - "title": "Model", - "type": "string" - }, - "operation": { - "title": "Operation", - "type": "string" - }, - "output_tokens": { - "title": "Output Tokens", - "type": "integer" - }, - "reasoning_tokens": { - "title": "Reasoning Tokens", - "type": "integer" - }, - "returned": { - "title": "Returned", - "type": "integer" - }, - "running": { - "title": "Running", - "type": "integer" - }, - "unreported": { - "title": "Unreported", - "type": "integer" - }, - "web_searches": { - "title": "Web Searches", + "version": { + "const": 1, + "default": 1, + "title": "Version", "type": "integer" } }, "required": [ - "date", - "operation", - "model", - "effort", - "calls", - "returned", - "failed", - "running", - "unreported", - "input_tokens", - "cached_input_tokens", - "output_tokens", - "reasoning_tokens", - "web_searches", - "duration_ms" + "base_url" ], - "title": "InferencePoint", + "title": "ConnectorConfig", "type": "object" }, - "IngestionStatus": { + "ConnectorFields": { + "additionalProperties": false, "properties": { - "article_enrichment_jobs": { - "additionalProperties": { - "type": "integer" + "description": { + "items": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "type": "string" }, - "title": "Article Enrichment Jobs", - "type": "object" + "maxItems": 5, + "minItems": 1, + "title": "Description", + "type": "array" }, - "image_jobs": { - "additionalProperties": { - "type": "integer" + "external_id": { + "items": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "type": "string" }, - "title": "Image Jobs", - "type": "object" + "maxItems": 5, + "minItems": 1, + "title": "External Id", + "type": "array" }, - "jobs": { - "additionalProperties": { - "type": "integer" + "listing_url": { + "items": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "type": "string" }, - "title": "Jobs", - "type": "object" + "maxItems": 5, + "minItems": 1, + "title": "Listing Url", + "type": "array" }, - "oldest_active_image_job_at": { + "name": { + "items": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "type": "string" + }, + "maxItems": 5, + "minItems": 1, + "title": "Name", + "type": "array" + }, + "pricing": { + "items": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "type": "string" + }, + "maxItems": 5, + "minItems": 1, + "title": "Pricing", + "type": "array" + }, + "product_url": { + "items": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "type": "string" + }, + "maxItems": 5, + "minItems": 1, + "title": "Product Url", + "type": "array" + } + }, + "title": "ConnectorFields", + "type": "object" + }, + "ConnectorFilter": { + "additionalProperties": false, + "properties": { + "include_missing": { + "default": true, + "title": "Include Missing", + "type": "boolean" + }, + "path": { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "title": "Path", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "maxItems": 20, + "minItems": 1, + "title": "Values", + "type": "array" + } + }, + "required": [ + "path", + "values" + ], + "title": "ConnectorFilter", + "type": "object" + }, + "ConnectorPagination": { + "additionalProperties": false, + "properties": { + "has_more_path": { "anyOf": [ { - "format": "date-time", + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", "type": "string" }, { "type": "null" } ], - "title": "Oldest Active Image Job At" + "title": "Has More Path" }, - "oldest_active_job_at": { + "mode": { + "default": "cursor", + "enum": [ + "none", + "cursor", + "page", + "offset", + "next_url" + ], + "title": "Mode", + "type": "string" + }, + "next_path": { + "default": "nextCursor", + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", + "title": "Next Path", + "type": "string" + }, + "page_size": { + "default": 10, + "maximum": 100.0, + "minimum": 1.0, + "title": "Page Size", + "type": "integer" + }, + "parameter": { "anyOf": [ { - "format": "date-time", + "maxLength": 100, + "minLength": 1, + "pattern": "^[A-Za-z][A-Za-z0-9_-]*$", "type": "string" }, { "type": "null" } ], - "title": "Oldest Active Job At" + "default": "cursor", + "title": "Parameter" }, - "queue_depth": { + "size_parameter": { "anyOf": [ { - "type": "integer" + "maxLength": 100, + "minLength": 1, + "pattern": "^[A-Za-z][A-Za-z0-9_-]*$", + "type": "string" }, { "type": "null" } ], - "title": "Queue Depth" + "default": "limit", + "title": "Size Parameter" }, - "redis_available": { - "title": "Redis Available", - "type": "boolean" + "start": { + "default": 1, + "maximum": 1000000.0, + "minimum": 0.0, + "title": "Start", + "type": "integer" }, - "scheduler_last_seen": { + "total_path": { "anyOf": [ { + "maxLength": 200, + "pattern": "^(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+)(?:\\.(?:[A-Za-z_][A-Za-z0-9_-]*|[0-9]+))*$|^$", "type": "string" }, { "type": "null" } ], - "title": "Scheduler Last Seen" - }, - "source_enrichment_jobs": { - "additionalProperties": { - "type": "integer" - }, - "title": "Source Enrichment Jobs", - "type": "object" + "title": "Total Path" } }, - "required": [ - "jobs", - "image_jobs", - "source_enrichment_jobs", - "article_enrichment_jobs", - "oldest_active_image_job_at", - "oldest_active_job_at", - "scheduler_last_seen", - "redis_available", - "queue_depth" - ], - "title": "IngestionStatus", + "title": "ConnectorPagination", "type": "object" }, - "JobLogEntry": { + "ConnectorPreview": { + "additionalProperties": false, "properties": { - "fields": { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "title": "Fields", - "type": "object" - }, - "id": { - "title": "Id", - "type": "string" - }, - "level": { - "enum": [ - "DEBUG", - "INFO", - "WARNING", - "ERROR", - "CRITICAL" - ], - "title": "Level", - "type": "string" - }, - "message": { - "title": "Message", - "type": "string" + "connector": { + "$ref": "#/components/schemas/ConnectorConfig" }, - "timestamp": { - "format": "date-time", - "title": "Timestamp", + "provider": { + "maxLength": 200, + "minLength": 1, + "title": "Provider", "type": "string" } }, "required": [ - "id", - "timestamp", - "level", - "message", - "fields" + "provider", + "connector" ], - "title": "JobLogEntry", + "title": "ConnectorPreview", "type": "object" }, - "JobOut": { + "ConnectorPreviewOut": { "properties": { - "articles_created": { - "title": "Articles Created", + "discovered": { + "title": "Discovered", "type": "integer" }, - "attempts": { - "title": "Attempts", - "type": "integer" + "errors": { + "items": { + "type": "string" + }, + "title": "Errors", + "type": "array" }, - "available_at": { - "format": "date-time", - "title": "Available At", - "type": "string" + "has_next_page": { + "title": "Has Next Page", + "type": "boolean" }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" + "products": { + "items": { + "$ref": "#/components/schemas/ProductInput" + }, + "title": "Products", + "type": "array" + } + }, + "required": [ + "products", + "errors", + "has_next_page", + "discovered" + ], + "title": "ConnectorPreviewOut", + "type": "object" + }, + "ConnectorResponseOut": { + "properties": { + "data": { + "$ref": "#/components/schemas/JsonValue" }, - "dispatched_at": { + "sampled": { + "title": "Sampled", + "type": "boolean" + } + }, + "required": [ + "data", + "sampled" + ], + "title": "ConnectorResponseOut", + "type": "object" + }, + "CreativePreviewOut": { + "properties": { + "product": { "anyOf": [ { - "format": "date-time", - "type": "string" + "$ref": "#/components/schemas/CreativePreviewProduct" }, { "type": "null" } - ], - "title": "Dispatched At" + ] + } + }, + "required": [ + "product" + ], + "title": "CreativePreviewOut", + "type": "object" + }, + "CreativePreviewProduct": { + "properties": { + "ad_image_status": { + "enum": [ + "default", + "queued", + "running", + "ready", + "failed" + ], + "title": "Ad Image Status", + "type": "string" }, - "entries_seen": { - "title": "Entries Seen", - "type": "integer" + "description": { + "title": "Description", + "type": "string" }, - "entries_skipped": { - "title": "Entries Skipped", - "type": "integer" + "id": { + "format": "uuid", + "title": "Id", + "type": "string" }, - "error": { + "image_url": { "anyOf": [ { "type": "string" @@ -4321,165 +5018,247 @@ "type": "null" } ], - "title": "Error" + "title": "Image Url" }, - "finished_at": { + "listing_url": { "anyOf": [ { - "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Finished At" + "title": "Listing Url" }, - "http_status": { + "logo_url": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Http Status" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" + "title": "Logo Url" }, - "source_id": { - "format": "uuid", - "title": "Source Id", + "name": { + "title": "Name", "type": "string" }, - "status": { - "title": "Status", + "product_url": { + "title": "Product Url", "type": "string" } }, "required": [ "id", - "source_id", - "status", - "attempts", - "created_at", - "available_at", - "dispatched_at", - "finished_at", - "http_status", - "entries_seen", - "articles_created", - "entries_skipped", - "error" + "name", + "product_url", + "listing_url", + "ad_image_status", + "description", + "logo_url", + "image_url" ], - "title": "JobOut", + "title": "CreativePreviewProduct", "type": "object" }, - "JsonValue": { - "anyOf": [ - { - "type": "string" + "Dashboard": { + "properties": { + "account": { + "$ref": "#/components/schemas/AccountOut" }, - { + "asset_total": { + "title": "Asset Total", "type": "integer" }, - { - "type": "number" + "assets": { + "items": { + "$ref": "#/components/schemas/AssetMetrics" + }, + "title": "Assets", + "type": "array" }, - { - "type": "boolean" + "end": { + "format": "date", + "title": "End", + "type": "string" }, - { + "format_totals": { "items": { - "$ref": "#/components/schemas/JsonValue" + "$ref": "#/components/schemas/FormatTotals" }, + "title": "Format Totals", "type": "array" }, - { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "type": "object" + "last_updated_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated At" }, - { - "type": "null" + "start": { + "format": "date", + "title": "Start", + "type": "string" + }, + "totals": { + "$ref": "#/components/schemas/Totals" + }, + "trend": { + "items": { + "$ref": "#/components/schemas/Trend" + }, + "title": "Trend", + "type": "array" } - ] + }, + "required": [ + "start", + "end", + "totals", + "trend", + "asset_total", + "last_updated_at", + "account", + "assets" + ], + "title": "Dashboard", + "type": "object" }, - "KeywordSuggestion": { + "DefaultSettings": { "additionalProperties": false, "properties": { - "article_count": { - "title": "Article Count", + "landing_page": { + "default": "/", + "enum": [ + "/", + "/content/articles", + "/taxonomy/topics", + "/jobs/analysis" + ], + "title": "Landing Page", + "type": "string" + }, + "overview_days": { + "default": 30, + "enum": [ + 7, + 30 + ], + "title": "Overview Days", "type": "integer" }, - "articles": { + "page_size": { + "default": 25, + "enum": [ + 10, + 25, + 50, + 100 + ], + "title": "Page Size", + "type": "integer" + }, + "refresh_seconds": { + "default": 10, + "enum": [ + 0, + 5, + 10, + 15, + 30, + 60 + ], + "title": "Refresh Seconds", + "type": "integer" + }, + "remember_columns": { + "default": true, + "title": "Remember Columns", + "type": "boolean" + }, + "remember_filters": { + "default": true, + "title": "Remember Filters", + "type": "boolean" + }, + "remember_sort": { + "default": true, + "title": "Remember Sort", + "type": "boolean" + } + }, + "title": "DefaultSettings", + "type": "object" + }, + "DeleteImportedPublishers": { + "additionalProperties": false, + "properties": { + "ids": { "items": { - "additionalProperties": { - "type": "string" - }, - "type": "object" + "format": "uuid", + "type": "string" }, - "title": "Articles", + "maxItems": 100, + "minItems": 1, + "title": "Ids", "type": "array" - }, - "keyword": { - "title": "Keyword", - "type": "string" } }, "required": [ - "keyword", - "article_count", - "articles" + "ids" ], - "title": "KeywordSuggestion", + "title": "DeleteImportedPublishers", "type": "object" }, - "LabelSelection": { - "additionalProperties": false, + "DeletedImportedPublishers": { "properties": { - "evidence": { - "maxLength": 500, - "minLength": 4, - "title": "Evidence", - "type": "string" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" + "deleted": { + "title": "Deleted", + "type": "integer" } }, "required": [ - "id", - "evidence" + "deleted" ], - "title": "LabelSelection", + "title": "DeletedImportedPublishers", "type": "object" }, - "NotificationConfig": { + "DestinationPreviewOut": { "properties": { - "enabled": { - "title": "Enabled", - "type": "boolean" - }, - "environment": { + "product": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/DestinationPreviewProduct" }, { "type": "null" } - ], - "title": "Environment" + ] + } + }, + "required": [ + "product" + ], + "title": "DestinationPreviewOut", + "type": "object" + }, + "DestinationPreviewProduct": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" }, - "subscriber_id": { + "listing_url": { "anyOf": [ { "type": "string" @@ -4488,429 +5267,546 @@ "type": "null" } ], - "title": "Subscriber Id" + "title": "Listing Url" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product_url": { + "title": "Product Url", + "type": "string" } }, "required": [ - "enabled" + "id", + "name", + "product_url", + "listing_url" ], - "title": "NotificationConfig", + "title": "DestinationPreviewProduct", "type": "object" }, - "NotificationSettings": { + "DevCardSettings": { "additionalProperties": false, + "description": "Content a user chooses to show on their public Dev Card.", "properties": { - "show_badge": { - "default": true, - "title": "Show Badge", - "type": "boolean" + "accent": { + "default": "default", + "enum": [ + "default", + "teal", + "violet", + "blue", + "amber", + "rose" + ], + "title": "Accent", + "type": "string" }, - "sound": { - "default": false, - "title": "Sound", - "type": "boolean" + "motion": { + "default": "animated", + "enum": [ + "static", + "animated" + ], + "title": "Motion", + "type": "string" + }, + "stats": { + "items": { + "enum": [ + "current_streak", + "longest_streak", + "total_reading_days" + ], + "type": "string" + }, + "maxItems": 3, + "title": "Stats", + "type": "array" + }, + "technologies": { + "anyOf": [ + { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Technologies" + }, + "theme": { + "default": "classic", + "enum": [ + "classic", + "terminal", + "aurora", + "minimal" + ], + "title": "Theme", + "type": "string" } }, - "title": "NotificationSettings", + "title": "DevCardSettings", "type": "object" }, - "OverviewActivity": { + "DeviceLogin": { "properties": { - "added": { - "title": "Added", - "type": "integer" + "expires_at": { + "format": "date-time", + "title": "Expires At", + "type": "string" }, - "date": { - "format": "date", - "title": "Date", + "login_id": { + "title": "Login Id", "type": "string" }, - "published": { - "title": "Published", - "type": "integer" - } - }, - "required": [ - "date", - "added", - "published" - ], - "title": "OverviewActivity", - "type": "object" - }, - "OverviewAdoption": { - "properties": { - "accounts": { - "default": 0, - "title": "Accounts", - "type": "integer" + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Message" }, - "following_sources": { - "default": 0, - "title": "Following Sources", - "type": "integer" + "status": { + "default": "pending", + "enum": [ + "pending", + "completed", + "failed", + "cancelled", + "expired" + ], + "title": "Status", + "type": "string" }, - "following_topics": { - "default": 0, - "title": "Following Topics", - "type": "integer" + "user_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Code" }, - "liking": { - "default": 0, - "title": "Liking", - "type": "integer" + "verification_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Verification Url" } }, - "title": "OverviewAdoption", + "required": [ + "login_id", + "expires_at" + ], + "title": "DeviceLogin", "type": "object" }, - "OverviewAnalysis": { + "EntryQuality": { + "additionalProperties": false, "properties": { - "failed": { - "title": "Failed", - "type": "integer" + "depth": { + "enum": [ + "substantial", + "light", + "uncertain" + ], + "title": "Depth", + "type": "string" }, - "queued": { - "title": "Queued", + "index": { + "maximum": 9.0, + "minimum": 0.0, + "title": "Index", "type": "integer" }, - "running": { - "title": "Running", - "type": "integer" + "promotion": { + "enum": [ + "primarily_promotional", + "editorial", + "uncertain" + ], + "title": "Promotion", + "type": "string" }, - "succeeded": { - "title": "Succeeded", - "type": "integer" + "quote": { + "maxLength": 500, + "title": "Quote", + "type": "string" + }, + "relevance": { + "enum": [ + "relevant", + "unrelated", + "uncertain" + ], + "title": "Relevance", + "type": "string" } }, "required": [ - "queued", - "running", - "succeeded", - "failed" + "index", + "relevance", + "depth", + "promotion", + "quote" ], - "title": "OverviewAnalysis", + "title": "EntryQuality", "type": "object" }, - "OverviewAnalysisActivity": { + "ErrorDetail": { "properties": { - "date": { - "format": "date", - "title": "Date", + "code": { + "title": "Code", "type": "string" }, - "failed": { - "title": "Failed", - "type": "integer" - }, - "succeeded": { - "title": "Succeeded", - "type": "integer" + "message": { + "title": "Message", + "type": "string" } }, "required": [ - "date", - "succeeded", - "failed" + "code", + "message" ], - "title": "OverviewAnalysisActivity", + "title": "ErrorDetail", "type": "object" }, - "OverviewInsights": { + "ErrorResponse": { "properties": { - "accounts": { - "$ref": "#/components/schemas/OverviewMetric" - }, - "adoption": { - "$ref": "#/components/schemas/OverviewAdoption" - }, - "analysis_average_seconds": { + "detail": { "anyOf": [ { - "type": "number" + "type": "string" }, { - "type": "null" - } - ], - "title": "Analysis Average Seconds" - }, - "coverage": { - "items": { - "$ref": "#/components/schemas/OverviewInterestCoverage" - }, - "title": "Coverage", - "type": "array" - }, - "failing_sources": { - "items": { - "$ref": "#/components/schemas/OverviewSourcePerformance" - }, - "title": "Failing Sources", - "type": "array" - }, - "oldest_review_at": { - "additionalProperties": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ] - }, - "title": "Oldest Review At", - "type": "object" - }, - "opens": { - "$ref": "#/components/schemas/OverviewMetric" - }, - "personalization": { - "$ref": "#/components/schemas/OverviewPersonalization" - }, - "processing": { - "items": { - "$ref": "#/components/schemas/OverviewProcessing" - }, - "title": "Processing", - "type": "array" - }, - "publication_p90_seconds": { - "anyOf": [ - { - "type": "number" + "$ref": "#/components/schemas/ErrorDetail" }, { - "type": "null" + "items": { + "$ref": "#/components/schemas/ValidationIssue" + }, + "type": "array" } ], - "title": "Publication P90 Seconds" - }, - "publication_seconds": { - "$ref": "#/components/schemas/OverviewMetric" - }, - "publications": { - "$ref": "#/components/schemas/OverviewMetric" - }, - "reader_activity": { - "items": { - "$ref": "#/components/schemas/OverviewReaderDay" - }, - "title": "Reader Activity", - "type": "array" - }, - "source_performance": { - "items": { - "$ref": "#/components/schemas/OverviewSourcePerformance" - }, - "title": "Source Performance", - "type": "array" - }, - "source_publications_total": { - "default": 0, - "title": "Source Publications Total", - "type": "integer" - }, - "top_articles": { - "items": { - "$ref": "#/components/schemas/OverviewPopularArticle" - }, - "title": "Top Articles", - "type": "array" - }, - "top_articles_days": { - "default": 30, - "title": "Top Articles Days", - "type": "integer" + "title": "Detail" } }, - "title": "OverviewInsights", + "required": [ + "detail" + ], + "title": "ErrorResponse", "type": "object" }, - "OverviewInterestCoverage": { + "EvaluationOut": { "properties": { - "followers": { - "title": "Followers", - "type": "integer" - }, - "id": { - "format": "uuid", - "title": "Id", + "created_at": { + "format": "date-time", + "title": "Created At", "type": "string" }, - "inferred_users": { - "title": "Inferred Users", - "type": "integer" + "current": { + "title": "Current", + "type": "boolean" }, - "last_published_at": { + "error": { "anyOf": [ { - "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Last Published At" + "title": "Error" }, - "logo_url": { + "finished_at": { "anyOf": [ { + "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Logo Url" + "title": "Finished At" }, - "name": { - "title": "Name", + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, - "publications": { - "title": "Publications", - "type": "integer" + "product_id": { + "format": "uuid", + "title": "Product Id", + "type": "string" + }, + "result": { + "anyOf": [ + { + "$ref": "#/components/schemas/EvaluationResult" + }, + { + "type": "null" + } + ] + }, + "reviews": { + "items": { + "$ref": "#/components/schemas/PartnerReviewEvent" + }, + "title": "Reviews", + "type": "array" + }, + "snapshot": { + "$ref": "#/components/schemas/EvaluationSnapshot" + }, + "status": { + "title": "Status", + "type": "string" } }, "required": [ "id", - "name", - "logo_url", - "followers", - "inferred_users", - "publications", - "last_published_at" + "product_id", + "status", + "created_at", + "finished_at", + "error", + "snapshot", + "result", + "reviews", + "current" ], - "title": "OverviewInterestCoverage", + "title": "EvaluationOut", "type": "object" }, - "OverviewMetric": { + "EvaluationResult": { + "additionalProperties": false, "properties": { - "current": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Current" - }, - "previous": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Previous" + "decisions": { + "items": { + "$ref": "#/components/schemas/MatchDecision" + }, + "maxItems": 20, + "title": "Decisions", + "type": "array" } }, - "title": "OverviewMetric", + "required": [ + "decisions" + ], + "title": "EvaluationResult", "type": "object" }, - "OverviewPanel": { + "EvaluationSnapshot": { "properties": { - "articles_pending_review": { - "default": 0, - "title": "Articles Pending Review", - "type": "integer" - }, - "automation": { - "anyOf": [ - { - "$ref": "#/components/schemas/OverviewPanelAutomation" - }, - { - "type": "null" - } - ] + "articles": { + "items": { + "$ref": "#/components/schemas/ArticleSnapshot" + }, + "title": "Articles", + "type": "array" }, - "days": { - "title": "Days", - "type": "integer" + "product": { + "$ref": "#/components/schemas/ProductSnapshot" }, - "generated_at": { - "format": "date-time", - "title": "Generated At", + "version": { + "title": "Version", "type": "string" - }, - "insights": { - "$ref": "#/components/schemas/OverviewInsights" - }, - "relationship_proposals_pending": { - "default": 0, - "title": "Relationship Proposals Pending", - "type": "integer" - }, - "sources_failing": { - "default": 0, - "title": "Sources Failing", - "type": "integer" - }, - "sources_pending_review": { - "default": 0, - "title": "Sources Pending Review", - "type": "integer" - }, - "topic_proposals_pending": { - "default": 0, - "title": "Topic Proposals Pending", - "type": "integer" } }, "required": [ - "generated_at", - "days" + "version", + "product", + "articles" ], - "title": "OverviewPanel", + "title": "EvaluationSnapshot", "type": "object" }, - "OverviewPanelAutomation": { + "Evidence": { + "additionalProperties": false, "properties": { - "analysis_duration_ms": { - "default": 0, - "title": "Analysis Duration Ms", - "type": "integer" + "capability": { + "maxLength": 500, + "minLength": 10, + "title": "Capability", + "type": "string" }, - "analysis_tokens": { - "default": 0, - "title": "Analysis Tokens", - "type": "integer" + "quote": { + "maxLength": 2000, + "minLength": 20, + "title": "Quote", + "type": "string" }, - "automatic_publication_percent": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Automatic Publication Percent" + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url", + "quote", + "capability" + ], + "title": "Evidence", + "type": "object" + }, + "FeedSettings": { + "additionalProperties": false, + "properties": { + "content_types": { + "items": { + "enum": [ + "article", + "news", + "tutorial", + "release", + "comparison", + "opinion" + ], + "type": "string" + }, + "maxItems": 6, + "minItems": 1, + "title": "Content Types", + "type": "array" }, - "blockers": { + "languages": { "items": { - "$ref": "#/components/schemas/AutomationBlocker" + "enum": [ + "en", + "af", + "sq", + "ar", + "hy", + "az", + "eu", + "be", + "bn", + "nb", + "bs", + "bg", + "ca", + "zh", + "hr", + "cs", + "da", + "nl", + "eo", + "et", + "fi", + "fr", + "lg", + "ka", + "de", + "el", + "gu", + "he", + "hi", + "hu", + "is", + "id", + "ga", + "it", + "ja", + "kk", + "ko", + "la", + "lv", + "lt", + "mk", + "ms", + "mi", + "mr", + "mn", + "nn", + "fa", + "pl", + "pt", + "pa", + "ro", + "ru", + "sr", + "sn", + "sk", + "sl", + "so", + "st", + "es", + "sw", + "sv", + "tl", + "ta", + "te", + "th", + "ts", + "tn", + "tr", + "uk", + "ur", + "vi", + "cy", + "xh", + "yo", + "zu" + ], + "type": "string" }, - "title": "Blockers", + "maxItems": 75, + "minItems": 1, + "title": "Languages", "type": "array" }, - "full_automation": { - "title": "Full Automation", - "type": "boolean" + "view": { + "default": "cards", + "enum": [ + "cards", + "compact" + ], + "title": "View", + "type": "string" + } + }, + "title": "FeedSettings", + "type": "object" + }, + "FormatTotals": { + "properties": { + "ad_format": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "title": "Ad Format", + "type": "string" }, - "inference": { - "$ref": "#/components/schemas/InferenceCharts" + "clicks": { + "title": "Clicks", + "type": "integer" }, - "median_ingestion_to_publication_seconds": { + "ctr": { "anyOf": [ { "type": "number" @@ -4919,422 +5815,489 @@ "type": "null" } ], - "title": "Median Ingestion To Publication Seconds" - }, - "published_in_window": { - "default": 0, - "title": "Published In Window", - "type": "integer" - }, - "published_without_intervention": { - "default": 0, - "title": "Published Without Intervention", - "type": "integer" - }, - "throughput": { - "$ref": "#/components/schemas/PipelineThroughput" - }, - "token_activity": { - "items": { - "$ref": "#/components/schemas/AnalysisTokenDay" - }, - "title": "Token Activity", - "type": "array" + "title": "Ctr" }, - "topic_decisions": { - "$ref": "#/components/schemas/TopicDecisionMetrics" - }, - "usage_reported_runs": { - "default": 0, - "title": "Usage Reported Runs", + "impressions": { + "title": "Impressions", "type": "integer" }, - "usage_unreported_runs": { - "default": 0, - "title": "Usage Unreported Runs", + "measured_days": { + "title": "Measured Days", "type": "integer" } }, - "title": "OverviewPanelAutomation", + "required": [ + "impressions", + "clicks", + "ctr", + "measured_days", + "ad_format" + ], + "title": "FormatTotals", "type": "object" }, - "OverviewPersonalization": { + "GitHubPull": { + "additionalProperties": false, "properties": { - "empty_with_interests": { + "offset": { "default": 0, - "title": "Empty With Interests", + "maximum": 10000.0, + "minimum": 0.0, + "title": "Offset", "type": "integer" }, - "expired": { - "default": 0, - "title": "Expired", + "revision": { + "anyOf": [ + { + "pattern": "^[a-f0-9]{40}$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Revision" + } + }, + "title": "GitHubPull", + "type": "object" + }, + "GitHubPullResult": { + "additionalProperties": false, + "properties": { + "created": { + "title": "Created", "type": "integer" }, "issues": { "items": { - "$ref": "#/components/schemas/OverviewUserIssue" + "type": "string" }, "title": "Issues", "type": "array" }, - "not_needed": { - "default": 0, - "title": "Not Needed", - "type": "integer" - }, - "overdue": { - "default": 0, - "title": "Overdue", - "type": "integer" + "next_offset": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Next Offset" }, - "pending": { - "default": 0, - "title": "Pending", + "processed": { + "title": "Processed", "type": "integer" }, - "ready": { - "default": 0, - "title": "Ready", - "type": "integer" + "revision": { + "pattern": "^[a-f0-9]{40}$", + "title": "Revision", + "type": "string" }, - "reasons": { - "items": { - "$ref": "#/components/schemas/OverviewReason" - }, - "title": "Reasons", - "type": "array" + "skipped": { + "title": "Skipped", + "type": "integer" }, - "refreshing": { - "default": 0, - "title": "Refreshing", + "total": { + "title": "Total", "type": "integer" } }, - "title": "OverviewPersonalization", + "required": [ + "revision", + "total", + "processed", + "created", + "skipped", + "issues", + "next_offset" + ], + "title": "GitHubPullResult", "type": "object" }, - "OverviewPopularArticle": { + "HealthResponse": { "properties": { - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "likes": { - "title": "Likes", - "type": "integer" - }, - "opens": { - "title": "Opens", - "type": "integer" - }, - "title": { - "title": "Title", + "status": { + "const": "ok", + "title": "Status", "type": "string" } }, "required": [ - "id", - "title", - "opens", - "likes" + "status" ], - "title": "OverviewPopularArticle", + "title": "HealthResponse", "type": "object" }, - "OverviewProcessing": { + "ImageVariant": { "properties": { - "completed": { - "title": "Completed", - "type": "integer" - }, - "duration_ms": { - "default": 0, - "title": "Duration Ms", - "type": "integer" - }, - "duration_runs": { - "default": 0, - "title": "Duration Runs", - "type": "integer" + "url": { + "title": "Url", + "type": "string" }, - "failed": { - "title": "Failed", + "width": { + "title": "Width", "type": "integer" - }, - "kind": { - "title": "Kind", + } + }, + "required": [ + "url", + "width" + ], + "title": "ImageVariant", + "type": "object" + }, + "ImportAction": { + "additionalProperties": false, + "properties": { + "action": { + "enum": [ + "discover", + "assess", + "select", + "approve", + "reject" + ], + "title": "Action", "type": "string" }, - "oldest_queued_at": { + "feed_id": { "anyOf": [ { - "format": "date-time", + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Oldest Queued At" - }, - "queued": { - "title": "Queued", - "type": "integer" + "title": "Feed Id" }, - "running": { - "title": "Running", - "type": "integer" + "note": { + "anyOf": [ + { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" } }, "required": [ - "kind", - "queued", - "running", - "completed", - "failed", - "oldest_queued_at" + "action" ], - "title": "OverviewProcessing", + "title": "ImportAction", "type": "object" }, - "OverviewReaderDay": { + "ImportAssessmentEvidence": { "properties": { - "accounts": { - "default": 0, - "title": "Accounts", - "type": "integer" - }, - "added": { - "default": 0, - "title": "Added", - "type": "integer" + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" }, - "content_types": { + "components": { "additionalProperties": { - "type": "integer" + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] }, - "title": "Content Types", + "title": "Components", "type": "object" }, - "date": { - "title": "Date", - "type": "string" - }, - "median_publication_seconds": { + "decision": { "anyOf": [ { - "type": "number" + "type": "string" }, { "type": "null" } ], - "title": "Median Publication Seconds" + "title": "Decision" }, - "multi_article_readers": { + "model_output": { "anyOf": [ { - "type": "integer" + "$ref": "#/components/schemas/Quality" + }, + { + "type": "null" + } + ] + }, + "reason": { + "anyOf": [ + { + "type": "string" }, { "type": "null" } ], - "title": "Multi Article Readers" + "title": "Reason" }, - "opens": { + "recommendation": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Opens" + "title": "Recommendation" }, - "published": { - "default": 0, - "title": "Published", - "type": "integer" + "sample": { + "items": { + "$ref": "#/components/schemas/ImportSample" + }, + "title": "Sample", + "type": "array" }, - "readers": { + "score": { "anyOf": [ { - "type": "integer" + "type": "number" }, { "type": "null" } ], - "title": "Readers" + "title": "Score" } }, - "required": [ - "date" - ], - "title": "OverviewReaderDay", + "title": "ImportAssessmentEvidence", "type": "object" }, - "OverviewReason": { + "ImportAssessmentOut": { "properties": { - "reason": { - "title": "Reason", + "created_at": { + "format": "date-time", + "title": "Created At", "type": "string" }, - "recommendations": { - "title": "Recommendations", - "type": "integer" + "evidence": { + "$ref": "#/components/schemas/ImportAssessmentEvidence" }, - "users": { - "title": "Users", - "type": "integer" - } - }, - "required": [ - "reason", - "users", - "recommendations" - ], - "title": "OverviewReason", - "type": "object" - }, - "OverviewSourcePerformance": { - "properties": { - "consecutive_failures": { - "title": "Consecutive Failures", - "type": "integer" + "feed_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Feed Id" }, - "discovered": { - "title": "Discovered", - "type": "integer" + "id": { + "format": "uuid", + "title": "Id", + "type": "string" }, - "fetch_failures": { - "title": "Fetch Failures", - "type": "integer" + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "id", + "version", + "feed_id", + "created_at", + "evidence" + ], + "title": "ImportAssessmentOut", + "type": "object" + }, + "ImportCandidateDetail": { + "properties": { + "approval_status": { + "title": "Approval Status", + "type": "string" }, - "followers": { - "title": "Followers", - "type": "integer" + "assessments": { + "items": { + "$ref": "#/components/schemas/ImportAssessmentOut" + }, + "title": "Assessments", + "type": "array" + }, + "feeds": { + "items": { + "$ref": "#/components/schemas/ImportFeedOut" + }, + "title": "Feeds", + "type": "array" }, "id": { "format": "uuid", "title": "Id", "type": "string" }, - "last_success_at": { + "identity_url": { + "title": "Identity Url", + "type": "string" + }, + "jobs": { + "items": { + "$ref": "#/components/schemas/ImportJobOut" + }, + "title": "Jobs", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provenance": { + "items": { + "$ref": "#/components/schemas/ImportProvenanceOut" + }, + "title": "Provenance", + "type": "array" + }, + "selected_feed_id": { "anyOf": [ { - "format": "date-time", + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Last Success At" + "title": "Selected Feed Id" }, - "logo_url": { + "source_id": { "anyOf": [ { + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Logo Url" + "title": "Source Id" }, - "name": { - "title": "Name", + "status": { + "title": "Status", "type": "string" }, - "published": { - "title": "Published", - "type": "integer" + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" } }, "required": [ "id", "name", - "logo_url", - "discovered", - "published", - "followers", - "fetch_failures", - "consecutive_failures", - "last_success_at" + "identity_url", + "status", + "approval_status", + "selected_feed_id", + "source_id", + "updated_at", + "feeds", + "assessments", + "jobs", + "provenance" ], - "title": "OverviewSourcePerformance", + "title": "ImportCandidateDetail", "type": "object" }, - "OverviewTopic": { + "ImportCandidateOut": { "properties": { - "articles": { - "title": "Articles", - "type": "integer" - }, - "id": { - "format": "uuid", - "title": "Id", + "approval_status": { + "title": "Approval Status", "type": "string" }, - "name": { - "title": "Name", - "type": "string" - } - }, - "required": [ - "id", - "name", - "articles" - ], - "title": "OverviewTopic", - "type": "object" - }, - "OverviewUserIssue": { - "properties": { "id": { "format": "uuid", "title": "Id", "type": "string" }, - "issue": { - "title": "Issue", + "identity_url": { + "title": "Identity Url", "type": "string" }, "name": { "title": "Name", "type": "string" }, - "next_refresh_at": { + "selected_feed_id": { "anyOf": [ { - "format": "date-time", + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Next Refresh At" + "title": "Selected Feed Id" + }, + "source_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" + }, + "status": { + "title": "Status", + "type": "string" + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" } }, "required": [ "id", "name", - "issue", - "next_refresh_at" + "identity_url", + "status", + "approval_status", + "selected_feed_id", + "source_id", + "updated_at" ], - "title": "OverviewUserIssue", + "title": "ImportCandidateOut", "type": "object" }, - "Page_AdminArticleOut_": { + "ImportCandidatePage": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/AdminArticleOut" + "$ref": "#/components/schemas/ImportCandidateOut" }, "title": "Items", "type": "array" @@ -5355,370 +6318,423 @@ "required": [ "items", "total", - "limit", - "offset" + "offset", + "limit" ], - "title": "Page[AdminArticleOut]", + "title": "ImportCandidatePage", "type": "object" }, - "Page_AdminJobOut_": { + "ImportFeedEvidence": { "properties": { - "items": { + "publisher_ratio": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Publisher Ratio" + }, + "publisher_type": { + "default": "unknown", + "title": "Publisher Type", + "type": "string" + }, + "recent": { + "default": false, + "title": "Recent", + "type": "boolean" + }, + "sample": { "items": { - "$ref": "#/components/schemas/AdminJobOut" + "$ref": "#/components/schemas/ImportSample" }, - "title": "Items", + "title": "Sample", "type": "array" }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" }, - "total": { - "title": "Total", + "usable_entries": { + "default": 0, + "title": "Usable Entries", "type": "integer" } }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[AdminJobOut]", + "title": "ImportFeedEvidence", "type": "object" }, - "Page_AdminTopicOut_": { + "ImportFeedOut": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminTopicOut" - }, - "title": "Items", - "type": "array" + "checked_at": { + "format": "date-time", + "title": "Checked At", + "type": "string" }, - "limit": { - "title": "Limit", - "type": "integer" + "evidence": { + "$ref": "#/components/schemas/ImportFeedEvidence" }, - "offset": { - "title": "Offset", - "type": "integer" + "id": { + "format": "uuid", + "title": "Id", + "type": "string" }, - "total": { - "title": "Total", - "type": "integer" + "url": { + "title": "Url", + "type": "string" } }, "required": [ - "items", - "total", - "limit", - "offset" + "id", + "url", + "evidence", + "checked_at" ], - "title": "Page[AdminTopicOut]", + "title": "ImportFeedOut", "type": "object" }, - "Page_AdminUserBookmark_": { + "ImportJobOut": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserBookmark" - }, - "title": "Items", - "type": "array" - }, - "limit": { - "title": "Limit", + "attempts": { + "title": "Attempts", "type": "integer" }, - "offset": { - "title": "Offset", - "type": "integer" + "available_at": { + "format": "date-time", + "title": "Available At", + "type": "string" }, - "total": { - "title": "Total", - "type": "integer" + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "lease_until": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lease Until" + }, + "stage": { + "title": "Stage", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" } }, "required": [ - "items", - "total", - "limit", - "offset" + "id", + "stage", + "status", + "attempts", + "error", + "available_at", + "lease_until" ], - "title": "Page[AdminUserBookmark]", + "title": "ImportJobOut", "type": "object" }, - "Page_AdminUserInterest_": { + "ImportProvenanceOut": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserInterest" - }, - "title": "Items", - "type": "array" - }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" }, - "total": { - "title": "Total", - "type": "integer" + "origin": { + "title": "Origin", + "type": "string" } }, "required": [ - "items", - "total", - "limit", - "offset" + "origin", + "created_at" ], - "title": "Page[AdminUserInterest]", + "title": "ImportProvenanceOut", "type": "object" }, - "Page_AdminUserLike_": { + "ImportSample": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserLike" - }, - "title": "Items", - "type": "array" + "published_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Published At" }, - "limit": { - "title": "Limit", - "type": "integer" + "summary": { + "title": "Summary", + "type": "string" }, - "offset": { - "title": "Offset", - "type": "integer" + "title": { + "title": "Title", + "type": "string" }, - "total": { - "title": "Total", - "type": "integer" + "url": { + "title": "Url", + "type": "string" } }, "required": [ - "items", - "total", - "limit", - "offset" + "title", + "summary", + "url" ], - "title": "Page[AdminUserLike]", + "title": "ImportSample", "type": "object" }, - "Page_AdminUserOut_": { + "InferenceCharts": { "properties": { - "items": { + "activity": { "items": { - "$ref": "#/components/schemas/AdminUserOut" + "$ref": "#/components/schemas/InferencePoint" }, - "title": "Items", + "title": "Activity", "type": "array" }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" + "first_recorded_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "First Recorded At" }, - "total": { - "title": "Total", - "type": "integer" + "topic_activity": { + "items": { + "$ref": "#/components/schemas/TopicDecisionDay" + }, + "title": "Topic Activity", + "type": "array" } }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[AdminUserOut]", + "title": "InferenceCharts", "type": "object" }, - "Page_AdminUserRead_": { + "InferencePoint": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserRead" - }, - "title": "Items", - "type": "array" + "cached_input_tokens": { + "title": "Cached Input Tokens", + "type": "integer" }, - "limit": { - "title": "Limit", + "calls": { + "title": "Calls", "type": "integer" }, - "offset": { - "title": "Offset", + "date": { + "title": "Date", + "type": "string" + }, + "duration_ms": { + "title": "Duration Ms", "type": "integer" }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[AdminUserRead]", - "type": "object" - }, - "Page_AdminUserReadingDay_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserReadingDay" - }, - "title": "Items", - "type": "array" + "effort": { + "title": "Effort", + "type": "string" }, - "limit": { - "title": "Limit", + "failed": { + "title": "Failed", "type": "integer" }, - "offset": { - "title": "Offset", + "input_tokens": { + "title": "Input Tokens", "type": "integer" }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[AdminUserReadingDay]", - "type": "object" - }, - "Page_AdminUserRecommendation_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserRecommendation" - }, - "title": "Items", - "type": "array" + "model": { + "title": "Model", + "type": "string" }, - "limit": { - "title": "Limit", + "operation": { + "title": "Operation", + "type": "string" + }, + "output_tokens": { + "title": "Output Tokens", "type": "integer" }, - "offset": { - "title": "Offset", + "reasoning_tokens": { + "title": "Reasoning Tokens", "type": "integer" }, - "total": { - "title": "Total", + "returned": { + "title": "Returned", "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[AdminUserRecommendation]", - "type": "object" - }, - "Page_AdminUserSource_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserSource" - }, - "title": "Items", - "type": "array" }, - "limit": { - "title": "Limit", + "running": { + "title": "Running", "type": "integer" }, - "offset": { - "title": "Offset", + "unreported": { + "title": "Unreported", "type": "integer" }, - "total": { - "title": "Total", + "web_searches": { + "title": "Web Searches", "type": "integer" } }, "required": [ - "items", - "total", - "limit", - "offset" + "date", + "operation", + "model", + "effort", + "calls", + "returned", + "failed", + "running", + "unreported", + "input_tokens", + "cached_input_tokens", + "output_tokens", + "reasoning_tokens", + "web_searches", + "duration_ms" ], - "title": "Page[AdminUserSource]", + "title": "InferencePoint", "type": "object" }, - "Page_AdminUserTopic_": { + "IngestionStatus": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/AdminUserTopic" + "article_enrichment_jobs": { + "additionalProperties": { + "type": "integer" }, - "title": "Items", - "type": "array" + "title": "Article Enrichment Jobs", + "type": "object" }, - "limit": { - "title": "Limit", - "type": "integer" + "image_jobs": { + "additionalProperties": { + "type": "integer" + }, + "title": "Image Jobs", + "type": "object" }, - "offset": { - "title": "Offset", - "type": "integer" + "jobs": { + "additionalProperties": { + "type": "integer" + }, + "title": "Jobs", + "type": "object" }, - "total": { - "title": "Total", - "type": "integer" + "oldest_active_image_job_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Oldest Active Image Job At" + }, + "oldest_active_job_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Oldest Active Job At" + }, + "queue_depth": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Queue Depth" + }, + "redis_available": { + "title": "Redis Available", + "type": "boolean" + }, + "scheduler_last_seen": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scheduler Last Seen" + }, + "source_enrichment_jobs": { + "additionalProperties": { + "type": "integer" + }, + "title": "Source Enrichment Jobs", + "type": "object" } }, "required": [ - "items", - "total", - "limit", - "offset" + "jobs", + "image_jobs", + "source_enrichment_jobs", + "article_enrichment_jobs", + "oldest_active_image_job_at", + "oldest_active_job_at", + "scheduler_last_seen", + "redis_available", + "queue_depth" ], - "title": "Page[AdminUserTopic]", + "title": "IngestionStatus", "type": "object" }, - "Page_ArticleReviewOut_": { + "InventoryOut": { "properties": { "items": { "items": { - "$ref": "#/components/schemas/ArticleReviewOut" + "$ref": "#/components/schemas/InventoryProductOut" }, "title": "Items", "type": "array" }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" - }, "total": { "title": "Total", "type": "integer" @@ -5726,437 +6742,292 @@ }, "required": [ "items", - "total", - "limit", - "offset" + "total" ], - "title": "Page[ArticleReviewOut]", + "title": "InventoryOut", "type": "object" }, - "Page_PublicationDecisionOut_": { + "InventoryProductOut": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/PublicationDecisionOut" - }, - "title": "Items", - "type": "array" + "active": { + "title": "Active", + "type": "boolean" }, - "limit": { - "title": "Limit", - "type": "integer" + "ad_formats": { + "anyOf": [ + { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Ad Formats" }, - "offset": { - "title": "Offset", - "type": "integer" + "ad_image_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ad Image Url" }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[PublicationDecisionOut]", - "type": "object" - }, - "Page_PublicationPolicyReviewOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/PublicationPolicyReviewOut" - }, - "title": "Items", - "type": "array" + "approval_source": { + "anyOf": [ + { + "enum": [ + "qualified", + "trusted" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Approval Source" }, - "limit": { - "title": "Limit", - "type": "integer" + "assessment": { + "$ref": "#/components/schemas/ProductAssessment" }, - "offset": { - "title": "Offset", - "type": "integer" + "commercial_approved": { + "default": false, + "title": "Commercial Approved", + "type": "boolean" }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[PublicationPolicyReviewOut]", - "type": "object" - }, - "Page_RelationOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/RelationOut" - }, - "title": "Items", - "type": "array" + "description": { + "title": "Description", + "type": "string" }, - "limit": { - "title": "Limit", - "type": "integer" + "editable": { + "title": "Editable", + "type": "boolean" }, - "offset": { - "title": "Offset", - "type": "integer" + "excluded": { + "title": "Excluded", + "type": "boolean" }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[RelationOut]", - "type": "object" - }, - "Page_RelationshipOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/RelationshipOut" - }, - "title": "Items", - "type": "array" + "globally_excluded": { + "title": "Globally Excluded", + "type": "boolean" }, - "limit": { - "title": "Limit", - "type": "integer" + "id": { + "format": "uuid", + "title": "Id", + "type": "string" }, - "offset": { - "title": "Offset", - "type": "integer" + "manual_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Manual Active" }, - "total": { - "title": "Total", + "name": { + "title": "Name", + "type": "string" + }, + "pricing": { + "enum": [ + "free", + "freemium", + "paid", + "unknown" + ], + "title": "Pricing", + "type": "string" + }, + "product_url": { + "title": "Product Url", + "type": "string" + }, + "promotion_surfaces": { + "anyOf": [ + { + "items": { + "enum": [ + "article", + "topic", + "feed" + ], + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Promotion Surfaces" + }, + "revision": { + "title": "Revision", "type": "integer" + }, + "source": { + "enum": [ + "manual", + "imported" + ], + "title": "Source", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "technologies": { + "items": { + "type": "string" + }, + "title": "Technologies", + "type": "array" } }, "required": [ - "items", - "total", - "limit", - "offset" + "id", + "name", + "technologies", + "status", + "excluded", + "globally_excluded", + "product_url", + "description", + "pricing", + "assessment", + "revision", + "source", + "editable", + "active", + "manual_active" ], - "title": "Page[RelationshipOut]", + "title": "InventoryProductOut", "type": "object" }, - "Page_RelationshipProposalOut_": { + "JobLogEntry": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/RelationshipProposalOut" + "fields": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" }, - "title": "Items", - "type": "array" + "title": "Fields", + "type": "object" }, - "limit": { - "title": "Limit", - "type": "integer" + "id": { + "title": "Id", + "type": "string" }, - "offset": { - "title": "Offset", - "type": "integer" + "level": { + "enum": [ + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL" + ], + "title": "Level", + "type": "string" }, - "total": { - "title": "Total", - "type": "integer" + "message": { + "title": "Message", + "type": "string" + }, + "timestamp": { + "format": "date-time", + "title": "Timestamp", + "type": "string" } }, "required": [ - "items", - "total", - "limit", - "offset" + "id", + "timestamp", + "level", + "message", + "fields" ], - "title": "Page[RelationshipProposalOut]", + "title": "JobLogEntry", "type": "object" }, - "Page_SourceOut_": { + "JobOut": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/SourceOut" - }, - "title": "Items", - "type": "array" - }, - "limit": { - "title": "Limit", + "articles_created": { + "title": "Articles Created", "type": "integer" }, - "offset": { - "title": "Offset", - "type": "integer" - }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[SourceOut]", - "type": "object" - }, - "Page_SourceReviewOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/SourceReviewOut" - }, - "title": "Items", - "type": "array" - }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" - }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[SourceReviewOut]", - "type": "object" - }, - "Page_TagOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/TagOut" - }, - "title": "Items", - "type": "array" - }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" - }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[TagOut]", - "type": "object" - }, - "Page_TopicProposalOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/TopicProposalOut" - }, - "title": "Items", - "type": "array" - }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" - }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[TopicProposalOut]", - "type": "object" - }, - "Page_TopicReplacementOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/TopicReplacementOut" - }, - "title": "Items", - "type": "array" - }, - "limit": { - "title": "Limit", - "type": "integer" - }, - "offset": { - "title": "Offset", - "type": "integer" - }, - "total": { - "title": "Total", - "type": "integer" - } - }, - "required": [ - "items", - "total", - "limit", - "offset" - ], - "title": "Page[TopicReplacementOut]", - "type": "object" - }, - "PipelineHour": { - "properties": { - "article_analyses": { - "default": 0, - "title": "Article Analyses", - "type": "integer" - }, - "articles_published": { - "default": 0, - "title": "Articles Published", + "attempts": { + "title": "Attempts", "type": "integer" }, - "hour": { + "available_at": { "format": "date-time", - "title": "Hour", + "title": "Available At", "type": "string" }, - "topic_decisions": { - "default": 0, - "title": "Topic Decisions", - "type": "integer" - } - }, - "required": [ - "hour" - ], - "title": "PipelineHour", - "type": "object" - }, - "PipelineQueue": { - "properties": { - "completed": { - "default": 0, - "title": "Completed", - "type": "integer" - }, - "failed": { - "default": 0, - "title": "Failed", - "type": "integer" - }, - "kind": { - "title": "Kind", + "created_at": { + "format": "date-time", + "title": "Created At", "type": "string" }, - "median_processing_seconds": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Median Processing Seconds" - }, - "oldest_due_seconds": { + "dispatched_at": { "anyOf": [ { - "type": "number" + "format": "date-time", + "type": "string" }, { "type": "null" } ], - "title": "Oldest Due Seconds" + "title": "Dispatched At" }, - "queued": { - "default": 0, - "title": "Queued", + "entries_seen": { + "title": "Entries Seen", "type": "integer" }, - "running": { - "default": 0, - "title": "Running", + "entries_skipped": { + "title": "Entries Skipped", "type": "integer" - } - }, - "required": [ - "kind" - ], - "title": "PipelineQueue", - "type": "object" - }, - "PipelineThroughput": { - "properties": { - "article_workers": { + }, + "error": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Article Workers" - }, - "articles_published_per_hour": { - "default": 0, - "title": "Articles Published Per Hour", - "type": "number" + "title": "Error" }, - "busy_article_workers": { + "finished_at": { "anyOf": [ { - "type": "integer" + "format": "date-time", + "type": "string" }, { "type": "null" } ], - "title": "Busy Article Workers" + "title": "Finished At" }, - "busy_topic_workers": { + "http_status": { "anyOf": [ { "type": "integer" @@ -6165,736 +7036,863 @@ "type": "null" } ], - "title": "Busy Topic Workers" + "title": "Http Status" }, - "capacity_observed": { - "default": false, - "title": "Capacity Observed", - "type": "boolean" + "id": { + "format": "uuid", + "title": "Id", + "type": "string" }, - "cooldown_seconds": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Cooldown Seconds" + "source_id": { + "format": "uuid", + "title": "Source Id", + "type": "string" }, - "eligible_article_workers": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Eligible Article Workers" + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "id", + "source_id", + "status", + "attempts", + "created_at", + "available_at", + "dispatched_at", + "finished_at", + "http_status", + "entries_seen", + "articles_created", + "entries_skipped", + "error" + ], + "title": "JobOut", + "type": "object" + }, + "JsonValue": { + "anyOf": [ + { + "type": "string" }, - "eligible_topic_workers": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Eligible Topic Workers" + { + "type": "integer" }, - "hours": { + { + "type": "number" + }, + { + "type": "boolean" + }, + { "items": { - "$ref": "#/components/schemas/PipelineHour" + "$ref": "#/components/schemas/JsonValue" }, - "title": "Hours", "type": "array" }, - "idle_article_workers": { + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "KeywordSuggestion": { + "additionalProperties": false, + "properties": { + "article_count": { + "title": "Article Count", + "type": "integer" + }, + "articles": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Articles", + "type": "array" + }, + "keyword": { + "title": "Keyword", + "type": "string" + } + }, + "required": [ + "keyword", + "article_count", + "articles" + ], + "title": "KeywordSuggestion", + "type": "object" + }, + "LabelSelection": { + "additionalProperties": false, + "properties": { + "evidence": { + "maxLength": 500, + "minLength": 4, + "title": "Evidence", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + } + }, + "required": [ + "id", + "evidence" + ], + "title": "LabelSelection", + "type": "object" + }, + "ListingOut": { + "properties": { + "account_id": { "anyOf": [ { - "type": "integer" + "format": "uuid", + "type": "string" }, { "type": "null" } ], - "title": "Idle Article Workers" + "title": "Account Id" }, - "idle_topic_workers": { + "account_name": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Idle Topic Workers" + "title": "Account Name" }, - "queues": { - "items": { - "$ref": "#/components/schemas/PipelineQueue" - }, - "title": "Queues", - "type": "array" + "active": { + "title": "Active", + "type": "boolean" }, - "shared_workers": { + "approval_source": { "anyOf": [ { - "type": "integer" + "enum": [ + "qualified", + "trusted" + ], + "type": "string" }, { "type": "null" } ], - "title": "Shared Workers" + "title": "Approval Source" }, - "topic_admission_limit": { - "default": 0, - "title": "Topic Admission Limit", - "type": "integer" + "attribution": { + "title": "Attribution", + "type": "string" }, - "topic_decisions_per_hour": { - "default": 0, - "title": "Topic Decisions Per Hour", - "type": "number" + "commercial_approved": { + "default": false, + "title": "Commercial Approved", + "type": "boolean" }, - "topic_workers": { + "connection_enabled": { + "title": "Connection Enabled", + "type": "boolean" + }, + "description": { + "title": "Description", + "type": "string" + }, + "external_id": { + "title": "External Id", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "identity_reason": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Topic Workers" + "title": "Identity Reason" + }, + "identity_status": { + "enum": [ + "resolved", + "unresolved" + ], + "title": "Identity Status", + "type": "string" + }, + "listing_url": { + "title": "Listing Url", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "platform_name": { + "title": "Platform Name", + "type": "string" + }, + "pricing": { + "title": "Pricing", + "type": "string" + }, + "product_url": { + "title": "Product Url", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" } }, - "title": "PipelineThroughput", + "required": [ + "id", + "provider", + "platform_name", + "external_id", + "name", + "product_url", + "listing_url", + "description", + "pricing", + "attribution", + "active", + "connection_enabled", + "identity_status", + "identity_reason", + "updated_at" + ], + "title": "ListingOut", "type": "object" }, - "ProfileLink": { + "ManualProductInput": { "additionalProperties": false, "properties": { - "label": { - "anyOf": [ - { - "maxLength": 80, - "pattern": "^[^\\x00]*$", - "type": "string" - }, - { - "type": "null" - } + "description": { + "maxLength": 5000, + "minLength": 10, + "title": "Description", + "type": "string" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "pricing": { + "default": "unknown", + "enum": [ + "free", + "freemium", + "paid", + "unknown" ], - "title": "Label" + "title": "Pricing", + "type": "string" }, - "url": { + "product_url": { "maxLength": 2048, - "title": "Url", + "title": "Product Url", "type": "string" } }, "required": [ - "url" + "name", + "product_url", + "description" ], - "title": "ProfileLink", + "title": "ManualProductInput", "type": "object" }, - "ProfileSettings": { - "additionalProperties": false, + "ManualProductSourceOut": { "properties": { - "avatar_url": { + "account_id": { + "format": "uuid", + "title": "Account Id", + "type": "string" + }, + "account_name": { + "title": "Account Name", + "type": "string" + }, + "active": { + "title": "Active", + "type": "boolean" + }, + "approval_source": { "anyOf": [ { - "maxLength": 2048, + "enum": [ + "qualified", + "trusted" + ], "type": "string" }, { "type": "null" } ], - "title": "Avatar Url" + "title": "Approval Source" }, - "display_name": { + "commercial_approved": { + "title": "Commercial Approved", + "type": "boolean" + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" + } + }, + "required": [ + "account_id", + "account_name", + "active", + "commercial_approved", + "approval_source", + "updated_at" + ], + "title": "ManualProductSourceOut", + "type": "object" + }, + "ManualProductUpdate": { + "additionalProperties": false, + "properties": { + "active": { "anyOf": [ { - "maxLength": 100, - "pattern": "^[^\\x00]*$", + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Active" + }, + "description": { + "anyOf": [ + { + "maxLength": 5000, + "minLength": 10, "type": "string" }, { "type": "null" } ], - "title": "Display Name" + "title": "Description" + }, + "expected_revision": { + "anyOf": [ + { + "minimum": 1.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Expected Revision" + }, + "name": { + "anyOf": [ + { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "pricing": { + "anyOf": [ + { + "enum": [ + "free", + "freemium", + "paid", + "unknown" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pricing" + }, + "product_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Url" } }, - "title": "ProfileSettings", + "title": "ManualProductUpdate", "type": "object" }, - "PublicationDecisionOut": { + "MatchDecision": { + "additionalProperties": false, "properties": { - "created_at": { - "format": "date-time", - "title": "Created At", + "article_id": { + "format": "uuid", + "title": "Article Id", "type": "string" }, - "decision": { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "title": "Decision", - "type": "object" + "article_quote": { + "maxLength": 1000, + "title": "Article Quote", + "type": "string" }, - "id": { - "format": "uuid", - "title": "Id", + "evidence_index": { + "maximum": 9.0, + "minimum": -1.0, + "title": "Evidence Index", + "type": "integer" + }, + "reason": { + "maxLength": 600, + "minLength": 10, + "title": "Reason", + "type": "string" + }, + "relevant": { + "title": "Relevant", + "type": "boolean" + }, + "technology": { + "maxLength": 200, + "title": "Technology", "type": "string" } }, "required": [ - "id", - "created_at", - "decision" + "article_id", + "relevant", + "reason", + "article_quote", + "evidence_index", + "technology" ], - "title": "PublicationDecisionOut", + "title": "MatchDecision", "type": "object" }, - "PublicationPolicyReviewOut": { + "MemberInput": { + "additionalProperties": false, "properties": { - "actor": { - "title": "Actor", - "type": "string" - }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" - }, - "id": { + "user_id": { "format": "uuid", - "title": "Id", + "title": "User Id", "type": "string" - }, - "mode": { - "title": "Mode", - "type": "string" - }, - "revision": { - "title": "Revision", - "type": "integer" } }, "required": [ - "id", - "created_at", - "mode", - "revision", - "actor" + "user_id" ], - "title": "PublicationPolicyReviewOut", + "title": "MemberInput", "type": "object" }, - "PublicationPolicyUpdate": { - "additionalProperties": false, + "MemberOut": { "properties": { - "expected_revision": { - "minimum": 0.0, - "title": "Expected Revision", - "type": "integer" + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Email" }, - "mode": { - "enum": [ - "manual", - "preview", - "auto" + "issuer": { + "title": "Issuer", + "type": "string" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } ], - "title": "Mode", + "title": "Name" + }, + "subject": { + "title": "Subject", "type": "string" + }, + "user_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id" } }, "required": [ - "mode", - "expected_revision" + "issuer", + "subject" ], - "title": "PublicationPolicyUpdate", + "title": "MemberOut", "type": "object" }, - "Quality": { - "additionalProperties": false, + "MetricInput": { "properties": { - "confidence": { - "maximum": 1.0, + "clicks": { + "maximum": 9.223372036854776e+18, "minimum": 0.0, - "title": "Confidence", - "type": "number" + "title": "Clicks", + "type": "integer" }, - "entries": { - "items": { - "$ref": "#/components/schemas/EntryQuality" - }, - "maxItems": 10, - "minItems": 1, - "title": "Entries", - "type": "array" + "day": { + "format": "date", + "title": "Day", + "type": "string" + }, + "impressions": { + "maximum": 9.223372036854776e+18, + "minimum": 0.0, + "title": "Impressions", + "type": "integer" } }, "required": [ - "confidence", - "entries" + "day", + "impressions", + "clicks" ], - "title": "Quality", + "title": "MetricInput", "type": "object" }, - "QueueOut": { + "NotificationConfig": { "properties": { - "busy_workers": { - "title": "Busy Workers", - "type": "integer" - }, - "dispatched": { - "title": "Dispatched", - "type": "integer" - }, - "failed": { - "default": 0, - "title": "Failed", - "type": "integer" - }, - "idle_workers": { - "title": "Idle Workers", - "type": "integer" - }, - "name": { - "title": "Name", - "type": "string" + "enabled": { + "title": "Enabled", + "type": "boolean" }, - "oldest_queued_at": { + "environment": { "anyOf": [ { - "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Oldest Queued At" - }, - "queued": { - "default": 0, - "title": "Queued", - "type": "integer" + "title": "Environment" }, - "registered_workers": { - "title": "Registered Workers", - "type": "integer" - }, - "review_required": { - "default": 0, - "title": "Review Required", - "type": "integer" - }, - "running": { - "default": 0, - "title": "Running", - "type": "integer" - }, - "succeeded": { - "default": 0, - "title": "Succeeded", - "type": "integer" - }, - "suspended_workers": { - "title": "Suspended Workers", - "type": "integer" + "subscriber_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Subscriber Id" } }, "required": [ - "name", - "registered_workers", - "busy_workers", - "idle_workers", - "suspended_workers", - "dispatched" + "enabled" ], - "title": "QueueOut", + "title": "NotificationConfig", "type": "object" }, - "RecoveryRequest": { + "NotificationSettings": { "additionalProperties": false, "properties": { - "expected_revision": { - "minimum": 0.0, - "title": "Expected Revision", - "type": "integer" + "show_badge": { + "default": true, + "title": "Show Badge", + "type": "boolean" + }, + "sound": { + "default": false, + "title": "Sound", + "type": "boolean" } }, - "required": [ - "expected_revision" - ], - "title": "RecoveryRequest", + "title": "NotificationSettings", "type": "object" }, - "RecoveryResult": { + "OverviewActivity": { "properties": { - "decision": { - "anyOf": [ - { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Decision" - }, - "job_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Job Id" + "added": { + "title": "Added", + "type": "integer" }, - "status": { - "title": "Status", + "date": { + "format": "date", + "title": "Date", "type": "string" + }, + "published": { + "title": "Published", + "type": "integer" } }, "required": [ - "status" + "date", + "added", + "published" ], - "title": "RecoveryResult", + "title": "OverviewActivity", "type": "object" }, - "RecoveryTarget": { + "OverviewAdoption": { "properties": { - "id": { - "format": "uuid", - "title": "Id", - "type": "string" + "accounts": { + "default": 0, + "title": "Accounts", + "type": "integer" }, - "kind": { - "enum": [ - "article", - "topic-proposal", - "relationship-proposal" - ], - "title": "Kind", - "type": "string" + "following_sources": { + "default": 0, + "title": "Following Sources", + "type": "integer" }, - "revision": { + "following_topics": { "default": 0, - "title": "Revision", + "title": "Following Topics", "type": "integer" }, - "title": { - "title": "Title", - "type": "string" + "liking": { + "default": 0, + "title": "Liking", + "type": "integer" } }, - "required": [ - "id", - "title", - "kind" - ], - "title": "RecoveryTarget", + "title": "OverviewAdoption", "type": "object" }, - "RelationOut": { + "OverviewAnalysis": { "properties": { - "evidence_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Evidence Url" + "failed": { + "title": "Failed", + "type": "integer" }, - "related_topic_id": { - "format": "uuid", - "title": "Related Topic Id", - "type": "string" + "queued": { + "title": "Queued", + "type": "integer" }, - "relation": { - "enum": [ - "uses_language", - "depends_on", - "implements", - "part_of", - "related_to" - ], - "title": "Relation", - "type": "string" + "running": { + "title": "Running", + "type": "integer" }, - "topic_id": { - "format": "uuid", - "title": "Topic Id", - "type": "string" + "succeeded": { + "title": "Succeeded", + "type": "integer" } }, "required": [ - "topic_id", - "related_topic_id", - "relation", - "evidence_url" + "queued", + "running", + "succeeded", + "failed" ], - "title": "RelationOut", + "title": "OverviewAnalysis", "type": "object" }, - "RelationWrite": { - "additionalProperties": false, + "OverviewAnalysisActivity": { "properties": { - "evidence_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Evidence Url" - }, - "related_topic_id": { - "format": "uuid", - "title": "Related Topic Id", + "date": { + "format": "date", + "title": "Date", "type": "string" }, - "relation": { - "enum": [ - "uses_language", - "depends_on", - "implements", - "part_of", - "related_to" - ], - "title": "Relation", - "type": "string" + "failed": { + "title": "Failed", + "type": "integer" }, - "topic_id": { - "format": "uuid", - "title": "Topic Id", - "type": "string" + "succeeded": { + "title": "Succeeded", + "type": "integer" } }, "required": [ - "topic_id", - "related_topic_id", - "relation" + "date", + "succeeded", + "failed" ], - "title": "RelationWrite", + "title": "OverviewAnalysisActivity", "type": "object" }, - "RelationshipAnalysisRequest": { - "additionalProperties": false, + "OverviewInsights": { "properties": { - "related_topic_id": { + "accounts": { + "$ref": "#/components/schemas/OverviewMetric" + }, + "adoption": { + "$ref": "#/components/schemas/OverviewAdoption" + }, + "analysis_average_seconds": { "anyOf": [ { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Related Topic Id" - } - }, - "title": "RelationshipAnalysisRequest", - "type": "object" - }, - "RelationshipOut": { - "properties": { - "evidence_url": { - "anyOf": [ - { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Evidence Url" + "title": "Analysis Average Seconds" }, - "proposal": { - "anyOf": [ - { - "$ref": "#/components/schemas/RelationshipProposalOut" - }, - { - "type": "null" - } - ] + "coverage": { + "items": { + "$ref": "#/components/schemas/OverviewInterestCoverage" + }, + "title": "Coverage", + "type": "array" }, - "related_topic_id": { - "format": "uuid", - "title": "Related Topic Id", - "type": "string" + "failing_sources": { + "items": { + "$ref": "#/components/schemas/OverviewSourcePerformance" + }, + "title": "Failing Sources", + "type": "array" }, - "related_topic_name": { - "title": "Related Topic Name", - "type": "string" + "oldest_review_at": { + "additionalProperties": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": "Oldest Review At", + "type": "object" }, - "relation": { - "enum": [ - "uses_language", - "depends_on", - "implements", - "part_of", - "related_to" - ], - "title": "Relation", - "type": "string" + "opens": { + "$ref": "#/components/schemas/OverviewMetric" }, - "status": { - "enum": [ - "approved", - "pending" - ], - "title": "Status", - "type": "string" + "personalization": { + "$ref": "#/components/schemas/OverviewPersonalization" }, - "topic_id": { - "format": "uuid", - "title": "Topic Id", - "type": "string" + "processing": { + "items": { + "$ref": "#/components/schemas/OverviewProcessing" + }, + "title": "Processing", + "type": "array" }, - "topic_name": { - "title": "Topic Name", - "type": "string" - } - }, - "required": [ - "topic_id", - "related_topic_id", - "topic_name", - "related_topic_name", - "relation", - "evidence_url", - "status", - "proposal" - ], - "title": "RelationshipOut", - "type": "object" - }, - "RelationshipProposalOut": { - "additionalProperties": false, - "properties": { - "approval_blocker": { + "publication_p90_seconds": { "anyOf": [ { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Approval Blocker" - }, - "can_approve": { - "title": "Can Approve", - "type": "boolean" + "title": "Publication P90 Seconds" }, - "content_hash": { - "title": "Content Hash", - "type": "string" + "publication_seconds": { + "$ref": "#/components/schemas/OverviewMetric" }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" + "publications": { + "$ref": "#/components/schemas/OverviewMetric" }, - "created_by": { - "additionalProperties": { - "type": "string" + "reader_activity": { + "items": { + "$ref": "#/components/schemas/OverviewReaderDay" }, - "title": "Created By", - "type": "object" + "title": "Reader Activity", + "type": "array" }, - "evidence_quote": { - "maxLength": 500, - "minLength": 4, - "title": "Evidence Quote", - "type": "string" + "source_performance": { + "items": { + "$ref": "#/components/schemas/OverviewSourcePerformance" + }, + "title": "Source Performance", + "type": "array" }, - "evidence_title": { - "maxLength": 300, - "minLength": 1, - "title": "Evidence Title", - "type": "string" + "source_publications_total": { + "default": 0, + "title": "Source Publications Total", + "type": "integer" }, - "evidence_url": { - "maxLength": 2048, - "title": "Evidence Url", - "type": "string" + "top_articles": { + "items": { + "$ref": "#/components/schemas/OverviewPopularArticle" + }, + "title": "Top Articles", + "type": "array" }, - "explanation": { - "maxLength": 1000, - "minLength": 1, - "title": "Explanation", - "type": "string" + "top_articles_days": { + "default": 30, + "title": "Top Articles Days", + "type": "integer" + } + }, + "title": "OverviewInsights", + "type": "object" + }, + "OverviewInterestCoverage": { + "properties": { + "followers": { + "title": "Followers", + "type": "integer" }, "id": { "format": "uuid", "title": "Id", "type": "string" }, - "job_id": { - "format": "uuid", - "title": "Job Id", - "type": "string" - }, - "related_topic_id": { - "format": "uuid", - "title": "Related Topic Id", - "type": "string" - }, - "related_topic_name": { - "title": "Related Topic Name", - "type": "string" - }, - "relation": { - "enum": [ - "uses_language", - "depends_on", - "implements", - "part_of", - "related_to" - ], - "title": "Relation", - "type": "string" - }, - "review_note": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Review Note" + "inferred_users": { + "title": "Inferred Users", + "type": "integer" }, - "reviewed_at": { + "last_published_at": { "anyOf": [ { "format": "date-time", @@ -6904,181 +7902,87 @@ "type": "null" } ], - "title": "Reviewed At" + "title": "Last Published At" }, - "reviewed_by": { + "logo_url": { "anyOf": [ { - "additionalProperties": { - "type": "string" - }, - "type": "object" + "type": "string" }, { "type": "null" } ], - "title": "Reviewed By" + "title": "Logo Url" }, - "status": { - "enum": [ - "pending", - "approved", - "rejected" - ], - "title": "Status", + "name": { + "title": "Name", "type": "string" }, - "topic_id": { - "format": "uuid", - "title": "Topic Id", - "type": "string" - }, - "topic_name": { - "title": "Topic Name", - "type": "string" + "publications": { + "title": "Publications", + "type": "integer" } }, "required": [ - "topic_id", - "related_topic_id", - "relation", - "explanation", - "evidence_url", - "evidence_title", - "evidence_quote", "id", - "job_id", - "topic_name", - "related_topic_name", - "status", - "created_at", - "created_by", - "reviewed_at", - "reviewed_by", - "review_note", - "content_hash", - "can_approve", - "approval_blocker" + "name", + "logo_url", + "followers", + "inferred_users", + "publications", + "last_published_at" ], - "title": "RelationshipProposalOut", + "title": "OverviewInterestCoverage", "type": "object" }, - "RelationshipReview": { - "additionalProperties": false, + "OverviewMetric": { "properties": { - "decision": { - "enum": [ - "approved", - "rejected" - ], - "title": "Decision", - "type": "string" - }, - "expected_input_hash": { - "pattern": "^[a-f0-9]{64}$", - "title": "Expected Input Hash", - "type": "string" - }, - "note": { + "current": { "anyOf": [ { - "maxLength": 1000, - "minLength": 1, - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Note" - } - }, - "required": [ - "decision", - "expected_input_hash" - ], - "title": "RelationshipReview", - "type": "object" - }, - "ReviewArticle": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "approve", - "reject", - "publish", - "unpublish" - ], - "title": "Action", - "type": "string" - }, - "expected_revision": { - "minimum": 0.0, - "title": "Expected Revision", - "type": "integer" + "title": "Current" }, - "note": { + "previous": { "anyOf": [ { - "maxLength": 1000, - "minLength": 1, - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Note" + "title": "Previous" } }, - "required": [ - "action", - "expected_revision" - ], - "title": "ReviewArticle", + "title": "OverviewMetric", "type": "object" }, - "ReviewSource": { - "additionalProperties": false, + "OverviewPanel": { "properties": { - "decision": { - "enum": [ - "approved", - "rejected" - ], - "title": "Decision", - "type": "string" + "articles_pending_review": { + "default": 0, + "title": "Articles Pending Review", + "type": "integer" }, - "note": { + "automation": { "anyOf": [ { - "maxLength": 1000, - "minLength": 1, - "type": "string" + "$ref": "#/components/schemas/OverviewPanelAutomation" }, { "type": "null" } - ], - "title": "Note" - } - }, - "required": [ - "decision" - ], - "title": "ReviewSource", - "type": "object" - }, - "SearchAnalytics": { - "properties": { - "enabled": { - "title": "Enabled", - "type": "boolean" + ] }, - "flush_interval_seconds": { - "title": "Flush Interval Seconds", + "days": { + "title": "Days", "type": "integer" }, "generated_at": { @@ -7086,326 +7990,387 @@ "title": "Generated At", "type": "string" }, - "nohits_queries": { - "items": { - "$ref": "#/components/schemas/SearchAnalyticsQuery" - }, - "title": "Nohits Queries", - "type": "array" + "insights": { + "$ref": "#/components/schemas/OverviewInsights" }, - "popular_queries": { - "items": { - "$ref": "#/components/schemas/SearchAnalyticsQuery" - }, - "title": "Popular Queries", - "type": "array" + "relationship_proposals_pending": { + "default": 0, + "title": "Relationship Proposals Pending", + "type": "integer" }, - "rules": { - "items": { - "type": "string" - }, - "title": "Rules", - "type": "array" - } - }, - "required": [ - "generated_at", - "enabled", - "flush_interval_seconds", - "rules", - "popular_queries", - "nohits_queries" - ], - "title": "SearchAnalytics", - "type": "object" - }, - "SearchAnalyticsQuery": { - "properties": { - "count": { - "minimum": 0.0, - "title": "Count", + "sources_failing": { + "default": 0, + "title": "Sources Failing", "type": "integer" }, - "query": { - "title": "Query", - "type": "string" + "sources_pending_review": { + "default": 0, + "title": "Sources Pending Review", + "type": "integer" + }, + "topic_proposals_pending": { + "default": 0, + "title": "Topic Proposals Pending", + "type": "integer" } }, "required": [ - "query", - "count" + "generated_at", + "days" ], - "title": "SearchAnalyticsQuery", + "title": "OverviewPanel", "type": "object" }, - "SearchSuggestionOut": { + "OverviewPanelAutomation": { "properties": { - "first_seen_at": { - "format": "date-time", - "title": "First Seen At", - "type": "string" - }, - "last_seen_at": { - "format": "date-time", - "title": "Last Seen At", - "type": "string" - }, - "query": { - "title": "Query", - "type": "string" + "analysis_duration_ms": { + "default": 0, + "title": "Analysis Duration Ms", + "type": "integer" }, - "query_hash": { - "title": "Query Hash", - "type": "string" + "analysis_tokens": { + "default": 0, + "title": "Analysis Tokens", + "type": "integer" }, - "review_note": { + "automatic_publication_percent": { "anyOf": [ { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Review Note" + "title": "Automatic Publication Percent" }, - "reviewed_at": { + "blockers": { + "items": { + "$ref": "#/components/schemas/AutomationBlocker" + }, + "title": "Blockers", + "type": "array" + }, + "full_automation": { + "title": "Full Automation", + "type": "boolean" + }, + "inference": { + "$ref": "#/components/schemas/InferenceCharts" + }, + "median_ingestion_to_publication_seconds": { "anyOf": [ { - "format": "date-time", - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Reviewed At" + "title": "Median Ingestion To Publication Seconds" }, - "status": { - "title": "Status", + "published_in_window": { + "default": 0, + "title": "Published In Window", + "type": "integer" + }, + "published_without_intervention": { + "default": 0, + "title": "Published Without Intervention", + "type": "integer" + }, + "throughput": { + "$ref": "#/components/schemas/PipelineThroughput" + }, + "token_activity": { + "items": { + "$ref": "#/components/schemas/AnalysisTokenDay" + }, + "title": "Token Activity", + "type": "array" + }, + "topic_decisions": { + "$ref": "#/components/schemas/TopicDecisionMetrics" + }, + "usage_reported_runs": { + "default": 0, + "title": "Usage Reported Runs", + "type": "integer" + }, + "usage_unreported_runs": { + "default": 0, + "title": "Usage Unreported Runs", + "type": "integer" + } + }, + "title": "OverviewPanelAutomation", + "type": "object" + }, + "OverviewPersonalization": { + "properties": { + "empty_with_interests": { + "default": 0, + "title": "Empty With Interests", + "type": "integer" + }, + "expired": { + "default": 0, + "title": "Expired", + "type": "integer" + }, + "issues": { + "items": { + "$ref": "#/components/schemas/OverviewUserIssue" + }, + "title": "Issues", + "type": "array" + }, + "not_needed": { + "default": 0, + "title": "Not Needed", + "type": "integer" + }, + "overdue": { + "default": 0, + "title": "Overdue", + "type": "integer" + }, + "pending": { + "default": 0, + "title": "Pending", + "type": "integer" + }, + "ready": { + "default": 0, + "title": "Ready", + "type": "integer" + }, + "reasons": { + "items": { + "$ref": "#/components/schemas/OverviewReason" + }, + "title": "Reasons", + "type": "array" + }, + "refreshing": { + "default": 0, + "title": "Refreshing", + "type": "integer" + } + }, + "title": "OverviewPersonalization", + "type": "object" + }, + "OverviewPopularArticle": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, - "successful_count": { - "title": "Successful Count", + "likes": { + "title": "Likes", + "type": "integer" + }, + "opens": { + "title": "Opens", "type": "integer" + }, + "title": { + "title": "Title", + "type": "string" } }, "required": [ - "query_hash", - "query", - "successful_count", - "status", - "first_seen_at", - "last_seen_at", - "reviewed_at", - "review_note" + "id", + "title", + "opens", + "likes" ], - "title": "SearchSuggestionOut", + "title": "OverviewPopularArticle", "type": "object" }, - "SearchSuggestionReview": { + "OverviewProcessing": { "properties": { - "note": { + "completed": { + "title": "Completed", + "type": "integer" + }, + "duration_ms": { + "default": 0, + "title": "Duration Ms", + "type": "integer" + }, + "duration_runs": { + "default": 0, + "title": "Duration Runs", + "type": "integer" + }, + "failed": { + "title": "Failed", + "type": "integer" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "oldest_queued_at": { "anyOf": [ { - "maxLength": 1000, + "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Note" + "title": "Oldest Queued At" }, - "status": { - "enum": [ - "approved", - "rejected" - ], - "title": "Status", - "type": "string" + "queued": { + "title": "Queued", + "type": "integer" + }, + "running": { + "title": "Running", + "type": "integer" } }, "required": [ - "status" + "kind", + "queued", + "running", + "completed", + "failed", + "oldest_queued_at" ], - "title": "SearchSuggestionReview", + "title": "OverviewProcessing", "type": "object" }, - "SourceImportRequest": { - "additionalProperties": false, + "OverviewReaderDay": { "properties": { - "content": { + "accounts": { + "default": 0, + "title": "Accounts", + "type": "integer" + }, + "added": { + "default": 0, + "title": "Added", + "type": "integer" + }, + "content_types": { + "additionalProperties": { + "type": "integer" + }, + "title": "Content Types", + "type": "object" + }, + "date": { + "title": "Date", + "type": "string" + }, + "median_publication_seconds": { "anyOf": [ { - "maxLength": 1000000, - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Content" + "title": "Median Publication Seconds" }, - "format": { - "enum": [ - "opml", - "urls", - "json", - "markdown" + "multi_article_readers": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } ], - "title": "Format", - "type": "string" + "title": "Multi Article Readers" }, - "name": { - "default": "Admin import", - "maxLength": 200, - "minLength": 1, - "title": "Name", - "type": "string" + "opens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Opens" }, - "url": { + "published": { + "default": 0, + "title": "Published", + "type": "integer" + }, + "readers": { "anyOf": [ { - "maxLength": 2048, - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Url" + "title": "Readers" } }, "required": [ - "format" + "date" ], - "title": "SourceImportRequest", + "title": "OverviewReaderDay", "type": "object" }, - "SourceImportResult": { + "OverviewReason": { "properties": { - "created": { - "title": "Created", - "type": "integer" + "reason": { + "title": "Reason", + "type": "string" }, - "existing": { - "title": "Existing", + "recommendations": { + "title": "Recommendations", "type": "integer" }, - "total": { - "title": "Total", + "users": { + "title": "Users", "type": "integer" } }, "required": [ - "created", - "existing", - "total" + "reason", + "users", + "recommendations" ], - "title": "SourceImportResult", + "title": "OverviewReason", "type": "object" }, - "SourceOut": { + "OverviewSourcePerformance": { "properties": { - "approval_status": { - "enum": [ - "pending", - "approved", - "rejected" - ], - "title": "Approval Status", - "type": "string" - }, "consecutive_failures": { "title": "Consecutive Failures", "type": "integer" }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "enabled": { - "title": "Enabled", - "type": "boolean" + "discovered": { + "title": "Discovered", + "type": "integer" }, - "feed_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Feed Url" + "fetch_failures": { + "title": "Fetch Failures", + "type": "integer" }, - "full_automation": { - "title": "Full Automation", - "type": "boolean" + "followers": { + "title": "Followers", + "type": "integer" }, "id": { "format": "uuid", "title": "Id", "type": "string" }, - "image_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Image Url" - }, - "language": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Language" - }, - "last_attempt_at": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Last Attempt At" - }, - "last_error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Last Error" - }, "last_success_at": { "anyOf": [ { @@ -7429,83 +8394,69 @@ ], "title": "Logo Url" }, - "logo_variants": { - "items": { - "$ref": "#/components/schemas/ImageVariant" - }, - "title": "Logo Variants", - "type": "array" - }, - "metadata_enriched_at": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Metadata Enriched At" - }, - "metadata_error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Metadata Error" - }, "name": { "title": "Name", "type": "string" }, - "next_fetch_at": { - "format": "date-time", - "title": "Next Fetch At", - "type": "string" - }, - "poll_interval_seconds": { - "title": "Poll Interval Seconds", + "published": { + "title": "Published", + "type": "integer" + } + }, + "required": [ + "id", + "name", + "logo_url", + "discovered", + "published", + "followers", + "fetch_failures", + "consecutive_failures", + "last_success_at" + ], + "title": "OverviewSourcePerformance", + "type": "object" + }, + "OverviewTopic": { + "properties": { + "articles": { + "title": "Articles", "type": "integer" }, - "publication_policy": { - "default": "manual", - "enum": [ - "manual", - "preview", - "auto" - ], - "title": "Publication Policy", + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, - "publication_policy_revision": { - "default": 0, - "title": "Publication Policy Revision", - "type": "integer" + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "id", + "name", + "articles" + ], + "title": "OverviewTopic", + "type": "object" + }, + "OverviewUserIssue": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" }, - "relevance_assessment": { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - }, - "title": "Relevance Assessment", - "type": "object" + "issue": { + "title": "Issue", + "type": "string" }, - "review_note": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Review Note" + "name": { + "title": "Name", + "type": "string" }, - "reviewed_at": { + "next_refresh_at": { "anyOf": [ { "format": "date-time", @@ -7515,1607 +8466,1195 @@ "type": "null" } ], - "title": "Reviewed At" - }, - "reviewed_by": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Reviewed By" - }, - "slug": { - "title": "Slug", - "type": "string" - }, - "source_type": { - "$ref": "#/components/schemas/SourceType" - }, - "submission_channel": { - "title": "Submission Channel", - "type": "string" - }, - "submitted_by": { - "anyOf": [ - { - "$ref": "#/components/schemas/SourceSubmitterOut" - }, - { - "type": "null" - } - ] - }, - "updated_at": { - "format": "date-time", - "title": "Updated At", - "type": "string" - }, - "website_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Website Url" + "title": "Next Refresh At" } }, "required": [ "id", - "slug", "name", - "source_type", - "approval_status", - "submitted_by", - "submission_channel", - "reviewed_at", - "reviewed_by", - "review_note", - "metadata_enriched_at", - "metadata_error", - "updated_at", - "feed_url", - "enabled", - "poll_interval_seconds", - "next_fetch_at", - "last_attempt_at", - "last_success_at", - "consecutive_failures", - "last_error", - "created_at" + "issue", + "next_refresh_at" ], - "title": "SourceOut", + "title": "OverviewUserIssue", "type": "object" }, - "SourcePatch": { - "additionalProperties": false, + "Page_AdminArticleOut_": { "properties": { - "description": { - "anyOf": [ - { - "maxLength": 500, - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "enabled": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Enabled" + "items": { + "items": { + "$ref": "#/components/schemas/AdminArticleOut" + }, + "title": "Items", + "type": "array" }, - "image_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Image Url" + "limit": { + "title": "Limit", + "type": "integer" }, - "language": { - "anyOf": [ - { - "maxLength": 35, - "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Language" + "offset": { + "title": "Offset", + "type": "integer" }, - "logo_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Logo Url" + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[AdminArticleOut]", + "type": "object" + }, + "Page_AdminJobOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/AdminJobOut" + }, + "title": "Items", + "type": "array" }, - "name": { - "anyOf": [ - { - "maxLength": 200, - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" + "limit": { + "title": "Limit", + "type": "integer" }, - "poll_interval_seconds": { - "anyOf": [ - { - "maximum": 604800.0, - "minimum": 300.0, - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Poll Interval Seconds" + "offset": { + "title": "Offset", + "type": "integer" }, - "website_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Website Url" + "total": { + "title": "Total", + "type": "integer" } }, - "title": "SourcePatch", + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[AdminJobOut]", "type": "object" }, - "SourcePreviewOut": { - "additionalProperties": false, + "Page_AdminTopicOut_": { "properties": { - "description": { - "anyOf": [ - { - "maxLength": 500, - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" + "items": { + "items": { + "$ref": "#/components/schemas/AdminTopicOut" + }, + "title": "Items", + "type": "array" }, - "entries_seen": { - "title": "Entries Seen", + "limit": { + "title": "Limit", "type": "integer" }, - "entries_skipped": { - "title": "Entries Skipped", + "offset": { + "title": "Offset", "type": "integer" }, - "image_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Image Url" - }, - "language": { - "anyOf": [ - { - "maxLength": 35, - "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Language" - }, - "logo_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Logo Url" - }, - "name": { - "maxLength": 200, - "minLength": 1, - "title": "Name", - "type": "string" - }, - "warnings": { + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[AdminTopicOut]", + "type": "object" + }, + "Page_AdminUserBookmark_": { + "properties": { + "items": { "items": { - "type": "string" + "$ref": "#/components/schemas/AdminUserBookmark" }, - "title": "Warnings", + "title": "Items", "type": "array" }, - "website_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Website Url" + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "name", - "entries_seen", - "entries_skipped", - "warnings" + "items", + "total", + "limit", + "offset" ], - "title": "SourcePreviewOut", + "title": "Page[AdminUserBookmark]", "type": "object" }, - "SourcePreviewRequest": { - "additionalProperties": false, + "Page_AdminUserInterest_": { "properties": { - "feed_url": { - "maxLength": 2048, - "title": "Feed Url", - "type": "string" + "items": { + "items": { + "$ref": "#/components/schemas/AdminUserInterest" + }, + "title": "Items", + "type": "array" }, - "source_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Source Id" + "limit": { + "title": "Limit", + "type": "integer" }, - "source_type": { - "$ref": "#/components/schemas/SourceType" + "offset": { + "title": "Offset", + "type": "integer" }, - "use_solver": { - "default": false, - "title": "Use Solver", - "type": "boolean" + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "feed_url", - "source_type" + "items", + "total", + "limit", + "offset" ], - "title": "SourcePreviewRequest", + "title": "Page[AdminUserInterest]", "type": "object" }, - "SourceRef": { + "Page_AdminUserLike_": { "properties": { - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "image_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Image Url" - }, - "language": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Language" - }, - "logo_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Logo Url" - }, - "logo_variants": { + "items": { "items": { - "$ref": "#/components/schemas/ImageVariant" + "$ref": "#/components/schemas/AdminUserLike" }, - "title": "Logo Variants", + "title": "Items", "type": "array" }, - "name": { - "title": "Name", - "type": "string" - }, - "slug": { - "title": "Slug", - "type": "string" + "limit": { + "title": "Limit", + "type": "integer" }, - "source_type": { - "$ref": "#/components/schemas/SourceType" + "offset": { + "title": "Offset", + "type": "integer" }, - "website_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Website Url" + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "id", - "slug", - "name", - "source_type" + "items", + "total", + "limit", + "offset" ], - "title": "SourceRef", + "title": "Page[AdminUserLike]", "type": "object" }, - "SourceReviewOut": { + "Page_AdminUserOut_": { "properties": { - "actor": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Actor" - }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" + "items": { + "items": { + "$ref": "#/components/schemas/AdminUserOut" + }, + "title": "Items", + "type": "array" }, - "decision": { - "title": "Decision", - "type": "string" + "limit": { + "title": "Limit", + "type": "integer" }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" + "offset": { + "title": "Offset", + "type": "integer" }, - "note": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Note" - }, - "source_id": { - "format": "uuid", - "title": "Source Id", - "type": "string" + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "id", - "source_id", - "decision", - "actor", - "note", - "created_at" + "items", + "total", + "limit", + "offset" ], - "title": "SourceReviewOut", + "title": "Page[AdminUserOut]", "type": "object" }, - "SourceSubmitter": { - "additionalProperties": false, + "Page_AdminUserRead_": { "properties": { - "name": { - "maxLength": 200, - "minLength": 1, - "title": "Name", - "type": "string" + "items": { + "items": { + "$ref": "#/components/schemas/AdminUserRead" + }, + "title": "Items", + "type": "array" }, - "profile_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Profile Url" + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "name" + "items", + "total", + "limit", + "offset" ], - "title": "SourceSubmitter", + "title": "Page[AdminUserRead]", "type": "object" }, - "SourceSubmitterOut": { - "additionalProperties": false, + "Page_AdminUserReadingDay_": { "properties": { - "name": { - "maxLength": 200, - "minLength": 1, - "title": "Name", - "type": "string" + "items": { + "items": { + "$ref": "#/components/schemas/AdminUserReadingDay" + }, + "title": "Items", + "type": "array" }, - "profile_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Profile Url" + "limit": { + "title": "Limit", + "type": "integer" }, - "user_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "User Id" + "offset": { + "title": "Offset", + "type": "integer" }, - "verified": { - "default": false, - "title": "Verified", - "type": "boolean" + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "name" + "items", + "total", + "limit", + "offset" ], - "title": "SourceSubmitterOut", + "title": "Page[AdminUserReadingDay]", "type": "object" }, - "SourceType": { - "enum": [ - "publisher", - "aggregator" - ], - "title": "SourceType", - "type": "string" - }, - "TableSettings": { - "additionalProperties": false, + "Page_AdminUserRecommendation_": { "properties": { - "columns": { - "maxProperties": 100, - "patternProperties": { - "^[a-zA-Z0-9_-]{1,80}$": { - "type": "boolean" - } + "items": { + "items": { + "$ref": "#/components/schemas/AdminUserRecommendation" }, - "title": "Columns", - "type": "object" + "title": "Items", + "type": "array" }, - "query": { - "maxProperties": 30, - "patternProperties": { - "^[a-zA-Z0-9_-]{1,80}$": { - "maxLength": 500, - "type": "string" - } - }, - "title": "Query", - "type": "object" + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" } }, - "title": "TableSettings", + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[AdminUserRecommendation]", "type": "object" }, - "TableSettingsPatch": { - "additionalProperties": false, + "Page_AdminUserSource_": { "properties": { - "columns": { - "anyOf": [ - { - "maxProperties": 100, - "patternProperties": { - "^[a-zA-Z0-9_-]{1,80}$": { - "type": "boolean" - } - }, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Columns" + "items": { + "items": { + "$ref": "#/components/schemas/AdminUserSource" + }, + "title": "Items", + "type": "array" }, - "query": { - "anyOf": [ - { - "maxProperties": 30, - "patternProperties": { - "^[a-zA-Z0-9_-]{1,80}$": { - "maxLength": 500, - "type": "string" - } - }, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Query" + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" } }, - "title": "TableSettingsPatch", + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[AdminUserSource]", "type": "object" }, - "TagOut": { + "Page_AdminUserTopic_": { "properties": { - "aliases": { + "items": { "items": { - "type": "string" + "$ref": "#/components/schemas/AdminUserTopic" }, - "title": "Aliases", + "title": "Items", "type": "array" }, - "auto_link_topic": { - "default": true, - "title": "Auto Link Topic", - "type": "boolean" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "name": { - "title": "Name", - "type": "string" - }, - "slug": { - "title": "Slug", - "type": "string" - }, - "topic_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Topic Id" + "limit": { + "title": "Limit", + "type": "integer" }, - "topic_match_checked_at": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Topic Match Checked At" + "offset": { + "title": "Offset", + "type": "integer" }, - "topic_match_status": { - "default": "pending", - "title": "Topic Match Status", - "type": "string" + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "id", - "name", - "slug", - "aliases" + "items", + "total", + "limit", + "offset" ], - "title": "TagOut", + "title": "Page[AdminUserTopic]", "type": "object" }, - "TagPatch": { - "additionalProperties": false, + "Page_ArticleReviewOut_": { "properties": { - "aliases": { - "anyOf": [ - { - "items": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "maxItems": 100, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Aliases" - }, - "auto_link_topic": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Auto Link Topic" + "items": { + "items": { + "$ref": "#/components/schemas/ArticleReviewOut" + }, + "title": "Items", + "type": "array" }, - "name": { - "anyOf": [ - { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" + "limit": { + "title": "Limit", + "type": "integer" }, - "slug": { - "anyOf": [ - { - "maxLength": 100, - "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Slug" + "offset": { + "title": "Offset", + "type": "integer" }, - "topic_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Topic Id" + "total": { + "title": "Total", + "type": "integer" } }, - "title": "TagPatch", + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[ArticleReviewOut]", "type": "object" }, - "TagWrite": { - "additionalProperties": false, + "Page_ConnectionOut_": { "properties": { - "aliases": { + "items": { "items": { - "maxLength": 100, - "minLength": 1, - "type": "string" + "$ref": "#/components/schemas/ConnectionOut" }, - "maxItems": 100, - "title": "Aliases", + "title": "Items", "type": "array" }, - "auto_link_topic": { - "default": true, - "title": "Auto Link Topic", - "type": "boolean" - }, - "name": { - "maxLength": 100, - "minLength": 1, - "title": "Name", - "type": "string" + "limit": { + "title": "Limit", + "type": "integer" }, - "slug": { - "maxLength": 100, - "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", - "title": "Slug", - "type": "string" + "offset": { + "title": "Offset", + "type": "integer" }, - "topic_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Topic Id" + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "name", - "slug" + "items", + "total", + "limit", + "offset" ], - "title": "TagWrite", + "title": "Page[ConnectionOut]", "type": "object" }, - "TopicAnalysisBatchOut": { - "additionalProperties": false, + "Page_PartnerEvaluationSummary_": { "properties": { - "already_active": { - "title": "Already Active", - "type": "integer" + "items": { + "items": { + "$ref": "#/components/schemas/PartnerEvaluationSummary" + }, + "title": "Items", + "type": "array" }, - "complete": { - "title": "Complete", + "limit": { + "title": "Limit", "type": "integer" }, - "pending": { - "title": "Pending", + "offset": { + "title": "Offset", "type": "integer" }, - "queued": { - "title": "Queued", + "total": { + "title": "Total", "type": "integer" } }, "required": [ - "queued", - "already_active", - "complete", - "pending" + "items", + "total", + "limit", + "offset" ], - "title": "TopicAnalysisBatchOut", + "title": "Page[PartnerEvaluationSummary]", "type": "object" }, - "TopicAnalysisOut": { - "additionalProperties": false, + "Page_PartnerJobOut_": { "properties": { - "attempts": { - "title": "Attempts", + "items": { + "items": { + "$ref": "#/components/schemas/PartnerJobOut" + }, + "title": "Items", + "type": "array" + }, + "limit": { + "title": "Limit", "type": "integer" }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" + "offset": { + "title": "Offset", + "type": "integer" }, - "error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error" - }, - "finished_at": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Finished At" - }, - "id": { - "format": "uuid", - "title": "Id", - "type": "string" - }, - "model": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Model" - }, - "outcome": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Outcome" - }, - "reasons": { + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[PartnerJobOut]", + "type": "object" + }, + "Page_PartnerPipelineOut_": { + "properties": { + "items": { "items": { - "type": "string" + "$ref": "#/components/schemas/PartnerPipelineOut" }, - "title": "Reasons", + "title": "Items", "type": "array" }, - "status": { - "enum": [ - "queued", - "running", - "succeeded", - "failed" - ], - "title": "Status", - "type": "string" + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" } }, "required": [ - "id", - "status", - "attempts", - "model", - "created_at", - "finished_at", - "outcome", - "error" + "items", + "total", + "limit", + "offset" ], - "title": "TopicAnalysisOut", + "title": "Page[PartnerPipelineOut]", "type": "object" }, - "TopicDecisionDay": { + "Page_ProductOut_": { "properties": { - "approved": { - "default": 0, - "title": "Approved", - "type": "integer" + "items": { + "items": { + "$ref": "#/components/schemas/ProductOut" + }, + "title": "Items", + "type": "array" }, - "calls": { - "default": 0, - "title": "Calls", + "limit": { + "title": "Limit", "type": "integer" }, - "date": { - "title": "Date", - "type": "string" + "offset": { + "title": "Offset", + "type": "integer" }, - "deferred": { - "default": 0, - "title": "Deferred", + "total": { + "title": "Total", "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[ProductOut]", + "type": "object" + }, + "Page_PublicationDecisionOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/PublicationDecisionOut" + }, + "title": "Items", + "type": "array" }, - "escalations": { - "default": 0, - "title": "Escalations", + "limit": { + "title": "Limit", "type": "integer" }, - "rejected": { - "default": 0, - "title": "Rejected", + "offset": { + "title": "Offset", "type": "integer" }, - "repeated_stages": { - "default": 0, - "title": "Repeated Stages", + "total": { + "title": "Total", "type": "integer" } }, "required": [ - "date" + "items", + "total", + "limit", + "offset" ], - "title": "TopicDecisionDay", + "title": "Page[PublicationDecisionOut]", "type": "object" }, - "TopicDecisionMetrics": { + "Page_PublicationPolicyReviewOut_": { "properties": { - "actionable": { - "default": 0, - "title": "Actionable", + "items": { + "items": { + "$ref": "#/components/schemas/PublicationPolicyReviewOut" + }, + "title": "Items", + "type": "array" + }, + "limit": { + "title": "Limit", "type": "integer" }, - "approved": { - "default": 0, - "title": "Approved", + "offset": { + "title": "Offset", "type": "integer" }, - "awaiting_review": { - "default": 0, - "title": "Awaiting Review", + "total": { + "title": "Total", "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[PublicationPolicyReviewOut]", + "type": "object" + }, + "Page_RelationOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/RelationOut" + }, + "title": "Items", + "type": "array" }, - "calls": { - "default": 0, - "title": "Calls", + "limit": { + "title": "Limit", "type": "integer" }, - "charged_tokens": { - "default": 0, - "title": "Charged Tokens", + "offset": { + "title": "Offset", "type": "integer" }, - "decisions_per_hour": { - "default": 0, - "title": "Decisions Per Hour", - "type": "number" + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[RelationOut]", + "type": "object" + }, + "Page_RelationshipOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/RelationshipOut" + }, + "title": "Items", + "type": "array" }, - "deferred": { - "default": 0, - "title": "Deferred", + "limit": { + "title": "Limit", "type": "integer" }, - "deferred_reasons": { - "additionalProperties": { - "type": "integer" - }, - "title": "Deferred Reasons", - "type": "object" + "offset": { + "title": "Offset", + "type": "integer" }, - "escalation_percent": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Escalation Percent" - }, - "escalations": { - "default": 0, - "title": "Escalations", + "total": { + "title": "Total", "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[RelationshipOut]", + "type": "object" + }, + "Page_RelationshipProposalOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/RelationshipProposalOut" + }, + "title": "Items", + "type": "array" }, - "estimated_drain_hours": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Estimated Drain Hours" - }, - "pending": { - "default": 0, - "title": "Pending", + "limit": { + "title": "Limit", "type": "integer" }, - "rejected": { - "default": 0, - "title": "Rejected", + "offset": { + "title": "Offset", "type": "integer" }, - "repeat_calls": { - "default": 0, - "title": "Repeat Calls", + "total": { + "title": "Total", "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[RelationshipProposalOut]", + "type": "object" + }, + "Page_SourceOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/SourceOut" + }, + "title": "Items", + "type": "array" }, - "tokens": { - "default": 0, - "title": "Tokens", + "limit": { + "title": "Limit", "type": "integer" }, - "tokens_per_completed_topic": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Tokens Per Completed Topic" + "offset": { + "title": "Offset", + "type": "integer" }, - "unreported_calls": { - "default": 0, - "title": "Unreported Calls", + "total": { + "title": "Total", "type": "integer" } }, - "title": "TopicDecisionMetrics", + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[SourceOut]", "type": "object" }, - "TopicDeleteImpact": { + "Page_SourceReviewOut_": { "properties": { - "articles": { - "title": "Articles", - "type": "integer" + "items": { + "items": { + "$ref": "#/components/schemas/SourceReviewOut" + }, + "title": "Items", + "type": "array" }, - "pending_topic_proposals": { - "title": "Pending Topic Proposals", + "limit": { + "title": "Limit", "type": "integer" }, - "published_articles": { - "title": "Published Articles", + "offset": { + "title": "Offset", "type": "integer" }, - "relationship_proposals": { - "title": "Relationship Proposals", + "total": { + "title": "Total", "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[SourceReviewOut]", + "type": "object" + }, + "Page_TagOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/TagOut" + }, + "title": "Items", + "type": "array" }, - "relationships": { - "title": "Relationships", + "limit": { + "title": "Limit", "type": "integer" }, - "research_jobs": { - "title": "Research Jobs", + "offset": { + "title": "Offset", "type": "integer" }, - "tags": { - "title": "Tags", + "total": { + "title": "Total", "type": "integer" } }, "required": [ - "articles", - "published_articles", - "tags", - "relationships", - "relationship_proposals", - "research_jobs", - "pending_topic_proposals" + "items", + "total", + "limit", + "offset" ], - "title": "TopicDeleteImpact", + "title": "Page[TagOut]", "type": "object" }, - "TopicDraft": { - "additionalProperties": false, - "description": "Reviewed topic fields; lifecycle status cannot be supplied by imports.", + "Page_TopicProposalOut_": { "properties": { - "aliases": { + "items": { "items": { - "maxLength": 100, - "minLength": 1, - "type": "string" + "$ref": "#/components/schemas/TopicProposalOut" }, - "maxItems": 50, - "title": "Aliases", + "title": "Items", "type": "array" }, - "description": { - "anyOf": [ - { - "maxLength": 2000, - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" + "limit": { + "title": "Limit", + "type": "integer" }, - "facts": { - "items": { - "$ref": "#/components/schemas/TopicFact" - }, - "maxItems": 50, - "title": "Facts", - "type": "array" + "offset": { + "title": "Offset", + "type": "integer" }, - "keywords": { + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[TopicProposalOut]", + "type": "object" + }, + "Page_TopicReplacementOut_": { + "properties": { + "items": { "items": { - "maxLength": 100, - "minLength": 1, - "type": "string" + "$ref": "#/components/schemas/TopicReplacementOut" }, - "maxItems": 100, - "title": "Keywords", + "title": "Items", "type": "array" }, - "kind": { - "enum": [ - "benchmark", - "competition", - "concept", - "dataset", - "database", - "discipline", - "event", - "format", - "framework", - "game", - "game_engine", - "hardware", - "language", - "library", - "license", - "model", - "operating_system", - "organization", - "package", - "platform", - "practice", - "product", - "program", - "protocol", - "publication", - "runtime", - "service", - "software", - "standard", - "technology", - "tool", - "unclassified" - ], - "title": "Kind", + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "title": "Page[TopicReplacementOut]", + "type": "object" + }, + "PartnerEvaluationDetail": { + "properties": { + "attempts": { + "title": "Attempts", + "type": "integer" + }, + "available_at": { + "format": "date-time", + "title": "Available At", "type": "string" }, - "logo_url": { + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "current": { + "title": "Current", + "type": "boolean" + }, + "error": { "anyOf": [ { - "maxLength": 2048, "type": "string" }, { "type": "null" } ], - "title": "Logo Url" - }, - "name": { - "maxLength": 100, - "minLength": 1, - "title": "Name", - "type": "string" - }, - "slug": { - "maxLength": 100, - "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", - "title": "Slug", - "type": "string" + "title": "Error" }, - "website_url": { + "finished_at": { "anyOf": [ { - "maxLength": 2048, + "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Website Url" - } - }, - "required": [ - "name", - "slug", - "kind" - ], - "title": "TopicDraft", - "type": "object" - }, - "TopicEnrichmentPreview": { - "additionalProperties": false, - "properties": { - "articles_examined": { - "title": "Articles Examined", - "type": "integer" + "title": "Finished At" }, - "preview_token": { - "title": "Preview Token", + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, - "suggestions": { - "items": { - "$ref": "#/components/schemas/KeywordSuggestion" - }, - "title": "Suggestions", - "type": "array" + "product_id": { + "format": "uuid", + "title": "Product Id", + "type": "string" }, - "topic": { - "$ref": "#/components/schemas/TopicDraft" - } - }, - "required": [ - "topic", - "preview_token", - "articles_examined", - "suggestions" - ], - "title": "TopicEnrichmentPreview", - "type": "object" - }, - "TopicEnrichmentSubmit": { - "additionalProperties": false, - "properties": { - "keywords": { + "result": { + "anyOf": [ + { + "$ref": "#/components/schemas/EvaluationResult" + }, + { + "type": "null" + } + ] + }, + "reviews": { "items": { - "maxLength": 100, - "minLength": 1, - "type": "string" + "$ref": "#/components/schemas/PartnerReviewEvent" }, - "maxItems": 25, - "minItems": 1, - "title": "Keywords", + "title": "Reviews", "type": "array" }, - "preview_token": { - "pattern": "^[a-f0-9]{64}$", - "title": "Preview Token", + "snapshot": { + "$ref": "#/components/schemas/EvaluationSnapshot" + }, + "status": { + "title": "Status", "type": "string" } }, "required": [ - "preview_token", - "keywords" + "id", + "product_id", + "status", + "created_at", + "finished_at", + "error", + "snapshot", + "result", + "reviews", + "current", + "attempts", + "available_at" ], - "title": "TopicEnrichmentSubmit", + "title": "PartnerEvaluationDetail", "type": "object" }, - "TopicFact": { - "additionalProperties": false, + "PartnerEvaluationSummary": { "properties": { - "name": { - "maxLength": 100, - "minLength": 1, - "title": "Name", - "type": "string" + "articles": { + "title": "Articles", + "type": "integer" }, - "retrieved_at": { + "attempts": { + "title": "Attempts", + "type": "integer" + }, + "available_at": { "format": "date-time", - "title": "Retrieved At", + "title": "Available At", "type": "string" }, - "source_url": { - "maxLength": 2048, - "title": "Source Url", + "created_at": { + "format": "date-time", + "title": "Created At", "type": "string" }, - "value": { - "maxLength": 500, - "minLength": 1, - "title": "Value", + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + }, + "finished_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Finished At" + }, + "id": { + "format": "uuid", + "title": "Id", "type": "string" - } - }, - "required": [ - "name", - "value", - "source_url", - "retrieved_at" - ], - "title": "TopicFact", - "type": "object" - }, - "TopicImport": { - "additionalProperties": false, - "properties": { - "content": { - "maxLength": 262144, - "minLength": 1, - "title": "Content", + }, + "matches": { + "title": "Matches", + "type": "integer" + }, + "product_id": { + "format": "uuid", + "title": "Product Id", "type": "string" }, - "format": { - "enum": [ - "json", - "csv" - ], - "title": "Format", + "product_name": { + "title": "Product Name", "type": "string" }, - "source_name": { - "maxLength": 200, - "minLength": 1, - "title": "Source Name", + "status": { + "title": "Status", "type": "string" } }, "required": [ - "format", - "content", - "source_name" + "id", + "product_id", + "product_name", + "status", + "attempts", + "created_at", + "available_at", + "finished_at", + "error", + "matches", + "articles" ], - "title": "TopicImport", + "title": "PartnerEvaluationSummary", "type": "object" }, - "TopicImportPreview": { - "additionalProperties": false, + "PartnerJobOut": { "properties": { - "can_submit": { - "title": "Can Submit", - "type": "boolean" - }, - "preview_token": { - "title": "Preview Token", - "type": "string" + "attempts": { + "title": "Attempts", + "type": "integer" }, - "rows": { - "items": { - "$ref": "#/components/schemas/TopicImportRow" - }, - "title": "Rows", - "type": "array" - } - }, - "required": [ - "rows", - "preview_token", - "can_submit" - ], - "title": "TopicImportPreview", - "type": "object" - }, - "TopicImportRow": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "create", - "update", - "unchanged", - "invalid" - ], - "title": "Action", + "created_at": { + "format": "date-time", + "title": "Created At", "type": "string" }, - "issues": { - "items": { - "type": "string" - }, - "title": "Issues", - "type": "array" - }, - "row": { - "title": "Row", - "type": "integer" - }, - "topic": { + "error": { "anyOf": [ { - "$ref": "#/components/schemas/TopicDraft" + "type": "string" }, { "type": "null" } - ] - } - }, - "required": [ - "row", - "action" - ], - "title": "TopicImportRow", - "type": "object" - }, - "TopicImportSubmit": { - "additionalProperties": false, - "properties": { - "content": { - "maxLength": 262144, - "minLength": 1, - "title": "Content", - "type": "string" - }, - "format": { - "enum": [ - "json", - "csv" - ], - "title": "Format", - "type": "string" - }, - "preview_token": { - "pattern": "^[a-f0-9]{64}$", - "title": "Preview Token", - "type": "string" - }, - "source_name": { - "maxLength": 200, - "minLength": 1, - "title": "Source Name", - "type": "string" - } - }, - "required": [ - "format", - "content", - "source_name", - "preview_token" - ], - "title": "TopicImportSubmit", - "type": "object" - }, - "TopicProposalFilterOptions": { - "properties": { - "kinds": { - "items": { - "type": "string" - }, - "title": "Kinds", - "type": "array" - }, - "sources": { - "items": { - "type": "string" - }, - "title": "Sources", - "type": "array" - } - }, - "required": [ - "kinds", - "sources" - ], - "title": "TopicProposalFilterOptions", - "type": "object" - }, - "TopicProposalOut": { - "additionalProperties": false, - "properties": { - "action": { - "enum": [ - "create", - "update" ], - "title": "Action", - "type": "string" + "title": "Error" }, - "analysis": { + "external_id": { "anyOf": [ { - "$ref": "#/components/schemas/TopicAnalysisOut" + "type": "string" }, { "type": "null" } - ] + ], + "title": "External Id" }, - "applied": { + "finished_at": { "anyOf": [ { - "$ref": "#/components/schemas/TopicDraft" + "format": "date-time", + "type": "string" }, { "type": "null" } - ] + ], + "title": "Finished At" }, - "batch_id": { + "id": { "format": "uuid", - "title": "Batch Id", + "title": "Id", "type": "string" }, - "before": { + "operation": { + "enum": [ + "sync", + "sync_product", + "assess" + ], + "title": "Operation", + "type": "string" + }, + "parent_id": { "anyOf": [ { - "$ref": "#/components/schemas/TopicDraft" + "format": "uuid", + "type": "string" }, { "type": "null" } - ] + ], + "title": "Parent Id" }, - "content_hash": { + "product_id": { "anyOf": [ { + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Content Hash" - }, - "created_at": { - "format": "date-time", - "title": "Created At", - "type": "string" - }, - "created_by": { - "additionalProperties": { - "type": "string" - }, - "title": "Created By", - "type": "object" + "title": "Product Id" }, - "evidence": { - "items": { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" + "provider": { + "anyOf": [ + { + "type": "string" }, - "type": "object" - }, - "title": "Evidence", - "type": "array" + { + "type": "null" + } + ], + "title": "Provider" }, - "id": { - "format": "uuid", - "title": "Id", + "status": { + "title": "Status", "type": "string" + } + }, + "required": [ + "id", + "provider", + "operation", + "parent_id", + "external_id", + "product_id", + "status", + "attempts", + "created_at", + "finished_at", + "error" + ], + "title": "PartnerJobOut", + "type": "object" + }, + "PartnerPipelineOut": { + "properties": { + "attempts": { + "title": "Attempts", + "type": "integer" }, - "origin": { - "enum": [ - "import", - "article_enrichment", - "ai_analysis" - ], - "title": "Origin", + "available_at": { + "format": "date-time", + "title": "Available At", "type": "string" }, - "proposed": { - "$ref": "#/components/schemas/TopicDraft" + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" }, - "review_note": { + "error": { "anyOf": [ { "type": "string" @@ -9124,48 +9663,46 @@ "type": "null" } ], - "title": "Review Note" + "title": "Error" }, - "reviewed_at": { + "external_id": { "anyOf": [ { - "format": "date-time", "type": "string" }, { "type": "null" } ], - "title": "Reviewed At" + "title": "External Id" }, - "reviewed_by": { + "finished_at": { "anyOf": [ { - "additionalProperties": { - "type": "string" - }, - "type": "object" + "format": "date-time", + "type": "string" }, { "type": "null" } ], - "title": "Reviewed By" - }, - "source_name": { - "title": "Source Name", - "type": "string" + "title": "Finished At" }, - "status": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "operation": { "enum": [ - "pending", - "approved", - "rejected" + "sync", + "sync_product", + "assess" ], - "title": "Status", + "title": "Operation", "type": "string" }, - "topic_id": { + "parent_id": { "anyOf": [ { "format": "uuid", @@ -9175,65 +9712,113 @@ "type": "null" } ], - "title": "Topic Id" + "title": "Parent Id" + }, + "product_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Id" + }, + "product_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Name" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" + }, + "status": { + "title": "Status", + "type": "string" } }, "required": [ "id", - "batch_id", - "topic_id", - "action", - "origin", - "source_name", - "proposed", - "before", - "evidence", + "provider", + "operation", + "parent_id", + "external_id", + "product_id", "status", + "attempts", "created_at", - "created_by", - "reviewed_at", - "reviewed_by", - "review_note", - "applied" + "finished_at", + "error", + "available_at", + "product_name" ], - "title": "TopicProposalOut", + "title": "PartnerPipelineOut", "type": "object" }, - "TopicReplacementOut": { + "PartnerProviderOut": { "properties": { - "id": { - "title": "Id", + "api_url": { + "title": "Api Url", "type": "string" }, - "kind": { - "title": "Kind", + "description": { + "title": "Description", "type": "string" }, "name": { "title": "Name", "type": "string" }, - "proposal_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Proposal Id" - }, - "slug": { - "title": "Slug", + "partnership_type": { + "const": "launch_platform", + "title": "Partnership Type", "type": "string" }, - "status": { - "title": "Status", + "provider": { + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider", + "name", + "partnership_type", + "api_url", + "description" + ], + "title": "PartnerProviderOut", + "type": "object" + }, + "PartnerReviewEvent": { + "properties": { + "action": { + "title": "Action", "type": "string" }, - "topic_id": { + "actor": { + "additionalProperties": { + "type": "string" + }, + "title": "Actor", + "type": "object" + }, + "article_id": { "anyOf": [ { "format": "uuid", @@ -9243,283 +9828,235 @@ "type": "null" } ], - "title": "Topic Id" - } - }, - "required": [ - "id", - "name", - "slug", - "kind", - "status", - "topic_id", - "proposal_id" - ], - "title": "TopicReplacementOut", - "type": "object" - }, - "TopicReview": { - "additionalProperties": false, - "properties": { - "decision": { - "enum": [ - "approved", - "rejected" - ], - "title": "Decision", + "title": "Article Id" + }, + "at": { + "format": "date-time", + "title": "At", "type": "string" }, - "expected_input_hash": { + "decision": { "anyOf": [ { - "pattern": "^[a-f0-9]{64}$", + "enum": [ + "accepted", + "rejected" + ], "type": "string" }, { "type": "null" } ], - "title": "Expected Input Hash" + "title": "Decision" }, - "note": { + "display_rights_confirmed": { "anyOf": [ { - "maxLength": 1000, - "minLength": 1, - "type": "string" + "type": "boolean" }, { "type": "null" } ], - "title": "Note" + "title": "Display Rights Confirmed" }, - "topic": { + "evidence_checked": { "anyOf": [ { - "$ref": "#/components/schemas/TopicDraft" + "type": "boolean" }, { "type": "null" } - ] + ], + "title": "Evidence Checked" + }, + "note": { + "title": "Note", + "type": "string" } }, "required": [ - "decision" + "actor", + "action", + "note", + "at" ], - "title": "TopicReview", + "title": "PartnerReviewEvent", "type": "object" }, - "TopicSelection": { - "additionalProperties": false, + "PartnershipTierOut": { "properties": { - "evidence": { - "maxLength": 500, - "minLength": 4, - "title": "Evidence", - "type": "string" + "advanced_targeting": { + "title": "Advanced Targeting", + "type": "boolean" }, - "relevance": { - "maximum": 1.0, - "minimum": 0.0, - "title": "Relevance", - "type": "number" + "allowed_ad_formats": { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "title": "Allowed Ad Formats", + "type": "array" }, - "role": { + "allowed_promotion_kinds": { + "items": { + "enum": [ + "product", + "ad" + ], + "type": "string" + }, + "title": "Allowed Promotion Kinds", + "type": "array" + }, + "benefits": { + "items": { + "type": "string" + }, + "title": "Benefits", + "type": "array" + }, + "default_promotion_kind": { "enum": [ - "primary", - "supporting", - "comparison", - "incidental" + "product", + "ad" ], - "title": "Role", + "title": "Default Promotion Kind", "type": "string" }, - "topic_id": { - "format": "uuid", - "title": "Topic Id", + "tier": { + "enum": [ + "bronze", + "silver", + "gold", + "platinum", + "diamond" + ], + "title": "Tier", "type": "string" } }, "required": [ - "topic_id", - "role", - "relevance", - "evidence" - ], - "title": "TopicSelection", + "tier", + "benefits", + "allowed_promotion_kinds", + "default_promotion_kind", + "advanced_targeting", + "allowed_ad_formats" + ], + "title": "PartnershipTierOut", "type": "object" }, - "UnhealthyResponse": { + "PipelineHour": { "properties": { - "status": { - "enum": [ - "migration_required", - "unavailable" - ], - "title": "Status", + "article_analyses": { + "default": 0, + "title": "Article Analyses", + "type": "integer" + }, + "articles_published": { + "default": 0, + "title": "Articles Published", + "type": "integer" + }, + "hour": { + "format": "date-time", + "title": "Hour", "type": "string" + }, + "topic_decisions": { + "default": 0, + "title": "Topic Decisions", + "type": "integer" } }, "required": [ - "status" + "hour" ], - "title": "UnhealthyResponse", + "title": "PipelineHour", "type": "object" }, - "UserAppearanceSettings": { - "additionalProperties": false, + "PipelineQueue": { "properties": { - "date_format": { - "default": "locale", - "enum": [ - "locale", - "iso", - "day-first", - "month-first" - ], - "title": "Date Format", - "type": "string" + "completed": { + "default": 0, + "title": "Completed", + "type": "integer" }, - "theme": { - "default": "system", - "enum": [ - "system", - "light", - "dark" - ], - "title": "Theme", - "type": "string" + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" }, - "time_format": { - "default": "system", - "enum": [ - "system", - "12", - "24" - ], - "title": "Time Format", + "kind": { + "title": "Kind", "type": "string" }, - "timezone": { - "default": "local", - "maxLength": 100, - "title": "Timezone", - "type": "string" - } - }, - "title": "UserAppearanceSettings", - "type": "object" - }, - "UserReadingStreak": { - "additionalProperties": false, - "properties": { - "current_days": { - "default": 0, - "title": "Current Days", - "type": "integer" + "median_processing_seconds": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Median Processing Seconds" }, - "last_read_date": { + "oldest_due_seconds": { "anyOf": [ { - "format": "date", - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Last Read Date" + "title": "Oldest Due Seconds" }, - "longest_days": { + "queued": { "default": 0, - "title": "Longest Days", + "title": "Queued", "type": "integer" }, - "total_days": { + "running": { "default": 0, - "title": "Total Days", + "title": "Running", "type": "integer" } }, - "title": "UserReadingStreak", - "type": "object" - }, - "UserSettings": { - "additionalProperties": false, - "properties": { - "appearance": { - "$ref": "#/components/schemas/AppearanceSettings" - }, - "defaults": { - "$ref": "#/components/schemas/DefaultSettings" - }, - "notifications": { - "$ref": "#/components/schemas/NotificationSettings" - }, - "profile": { - "$ref": "#/components/schemas/ProfileSettings" - }, - "tables": { - "additionalProperties": { - "$ref": "#/components/schemas/TableSettings" - }, - "title": "Tables", - "type": "object" - } - }, - "title": "UserSettings", - "type": "object" - }, - "ValidationIssue": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "title": "Loc", - "type": "array" - }, - "msg": { - "title": "Msg", - "type": "string" - }, - "type": { - "title": "Type", - "type": "string" - } - }, "required": [ - "loc", - "msg", - "type" + "kind" ], - "title": "ValidationIssue", + "title": "PipelineQueue", "type": "object" }, - "WorkerJob": { + "PipelineThroughput": { "properties": { - "article_id": { + "article_workers": { "anyOf": [ { - "format": "uuid", - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Article Id" + "title": "Article Workers" }, - "elapsed_seconds": { + "articles_published_per_hour": { + "default": 0, + "title": "Articles Published Per Hour", + "type": "number" + }, + "busy_article_workers": { "anyOf": [ { "type": "integer" @@ -9528,155 +10065,213 @@ "type": "null" } ], - "title": "Elapsed Seconds" + "title": "Busy Article Workers" }, - "id": { + "busy_topic_workers": { "anyOf": [ { - "format": "uuid", - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Id" + "title": "Busy Topic Workers" }, - "kind": { + "capacity_observed": { + "default": false, + "title": "Capacity Observed", + "type": "boolean" + }, + "cooldown_seconds": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Kind" + "title": "Cooldown Seconds" }, - "outcome": { + "eligible_article_workers": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Outcome" + "title": "Eligible Article Workers" }, - "proposal_id": { + "eligible_topic_workers": { "anyOf": [ { - "format": "uuid", - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Proposal Id" + "title": "Eligible Topic Workers" }, - "rq_id": { - "title": "Rq Id", - "type": "string" + "hours": { + "items": { + "$ref": "#/components/schemas/PipelineHour" + }, + "title": "Hours", + "type": "array" }, - "source_id": { + "idle_article_workers": { "anyOf": [ { - "format": "uuid", - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Source Id" + "title": "Idle Article Workers" }, - "started_at": { + "idle_topic_workers": { "anyOf": [ { - "format": "date-time", - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Started At" + "title": "Idle Topic Workers" }, - "status": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" + "queues": { + "items": { + "$ref": "#/components/schemas/PipelineQueue" + }, + "title": "Queues", + "type": "array" }, - "target_name": { + "shared_workers": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Target Name" + "title": "Shared Workers" }, - "topic_id": { + "topic_admission_limit": { + "default": 0, + "title": "Topic Admission Limit", + "type": "integer" + }, + "topic_decisions_per_hour": { + "default": 0, + "title": "Topic Decisions Per Hour", + "type": "number" + }, + "topic_workers": { "anyOf": [ { - "format": "uuid", - "type": "string" + "type": "integer" }, { "type": "null" } ], - "title": "Topic Id" + "title": "Topic Workers" } }, - "required": [ - "rq_id" - ], - "title": "WorkerJob", + "title": "PipelineThroughput", "type": "object" }, - "WorkerOut": { + "ProductAction": { + "additionalProperties": false, "properties": { - "completed_executions": { - "title": "Completed Executions", - "type": "integer" + "action": { + "enum": [ + "exclude", + "retry" + ], + "title": "Action", + "type": "string" }, - "current_job": { - "anyOf": [ - { - "$ref": "#/components/schemas/WorkerJob" - }, - { - "type": "null" - } - ] - }, - "failed_executions": { - "title": "Failed Executions", + "expected_revision": { + "minimum": 1.0, + "title": "Expected Revision", "type": "integer" + } + }, + "required": [ + "action", + "expected_revision" + ], + "title": "ProductAction", + "type": "object" + }, + "ProductAssessment": { + "properties": { + "reason": { + "default": "", + "title": "Reason", + "type": "string" }, - "heartbeat_age_seconds": { + "state": { + "default": "waiting", + "enum": [ + "waiting", + "checking", + "qualified", + "attention", + "irrelevant", + "withdrawn" + ], + "title": "State", + "type": "string" + } + }, + "title": "ProductAssessment", + "type": "object" + }, + "ProductDetailOut": { + "properties": { + "account_id": { + "format": "uuid", + "title": "Account Id", + "type": "string" + }, + "account_name": { + "title": "Account Name", + "type": "string" + }, + "active": { + "title": "Active", + "type": "boolean" + }, + "ad_formats": { "anyOf": [ { - "type": "integer" + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "type": "array" }, { "type": "null" } ], - "title": "Heartbeat Age Seconds" + "title": "Ad Formats" }, - "hostname": { + "ad_image_url": { "anyOf": [ { "type": "string" @@ -9685,173 +10280,8215 @@ "type": "null" } ], - "title": "Hostname" + "title": "Ad Image Url" }, - "last_heartbeat": { + "approval_source": { "anyOf": [ { - "format": "date-time", + "enum": [ + "qualified", + "trusted" + ], "type": "string" }, { "type": "null" } ], - "title": "Last Heartbeat" + "title": "Approval Source" }, - "name": { - "title": "Name", + "assessment": { + "$ref": "#/components/schemas/ProductAssessment" + }, + "commercial_approved": { + "default": false, + "title": "Commercial Approved", + "type": "boolean" + }, + "description": { + "title": "Description", "type": "string" }, - "pid": { + "editable": { + "title": "Editable", + "type": "boolean" + }, + "excluded": { + "title": "Excluded", + "type": "boolean" + }, + "globally_excluded": { + "title": "Globally Excluded", + "type": "boolean" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "manual_active": { "anyOf": [ { - "type": "integer" + "type": "boolean" }, { "type": "null" } ], - "title": "Pid" - }, - "queues": { - "items": { - "type": "string" - }, - "title": "Queues", - "type": "array" - }, - "registered": { - "title": "Registered", - "type": "boolean" + "title": "Manual Active" }, - "registration_ttl_seconds": { - "title": "Registration Ttl Seconds", - "type": "integer" + "name": { + "title": "Name", + "type": "string" }, - "role": { + "pricing": { "enum": [ - "ai", - "background", - "mixed", - "solver" + "free", + "freemium", + "paid", + "unknown" ], - "title": "Role", + "title": "Pricing", "type": "string" }, - "started_at": { + "product_url": { + "title": "Product Url", + "type": "string" + }, + "promotion_surfaces": { "anyOf": [ { - "format": "date-time", - "type": "string" + "items": { + "enum": [ + "article", + "topic", + "feed" + ], + "type": "string" + }, + "type": "array" }, { "type": "null" } ], - "title": "Started At" + "title": "Promotion Surfaces" }, - "state": { - "title": "State", + "revision": { + "title": "Revision", + "type": "integer" + }, + "source": { + "enum": [ + "manual", + "imported" + ], + "title": "Source", "type": "string" }, - "uptime_seconds": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } + "status": { + "title": "Status", + "type": "string" + }, + "technologies": { + "items": { + "type": "string" + }, + "title": "Technologies", + "type": "array" + } + }, + "required": [ + "id", + "name", + "technologies", + "status", + "excluded", + "globally_excluded", + "product_url", + "description", + "pricing", + "assessment", + "revision", + "source", + "editable", + "active", + "manual_active", + "account_id", + "account_name" + ], + "title": "ProductDetailOut", + "type": "object" + }, + "ProductInput": { + "additionalProperties": false, + "properties": { + "attribution": { + "default": "", + "maxLength": 300, + "title": "Attribution", + "type": "string" + }, + "description": { + "maxLength": 5000, + "minLength": 10, + "title": "Description", + "type": "string" + }, + "evidence": { + "items": { + "$ref": "#/components/schemas/Evidence" + }, + "maxItems": 10, + "title": "Evidence", + "type": "array" + }, + "external_id": { + "maxLength": 200, + "minLength": 1, + "title": "External Id", + "type": "string" + }, + "listing_url": { + "title": "Listing Url", + "type": "string" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "pricing": { + "default": "unknown", + "enum": [ + "free", + "freemium", + "paid", + "unknown" ], - "title": "Uptime Seconds" + "title": "Pricing", + "type": "string" }, - "working_seconds": { - "title": "Working Seconds", - "type": "number" + "product_url": { + "title": "Product Url", + "type": "string" + }, + "provider": { + "maxLength": 200, + "minLength": 1, + "title": "Provider", + "type": "string" + }, + "technologies": { + "items": { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + "maxItems": 20, + "title": "Technologies", + "type": "array" } }, "required": [ "name", - "role", - "queues", - "state", - "registered", - "hostname", - "pid", - "started_at", - "last_heartbeat", - "uptime_seconds", - "heartbeat_age_seconds", - "registration_ttl_seconds", - "completed_executions", - "failed_executions", - "working_seconds" + "product_url", + "description", + "provider", + "external_id", + "listing_url" ], - "title": "WorkerOut", + "title": "ProductInput", "type": "object" }, - "WorkersSnapshot": { + "ProductOut": { + "additionalProperties": false, "properties": { - "ai_cooldown_seconds": { - "title": "Ai Cooldown Seconds", - "type": "integer" + "approval_sources": { + "items": { + "enum": [ + "qualified", + "trusted" + ], + "type": "string" + }, + "title": "Approval Sources", + "type": "array" }, - "generated_at": { - "format": "date-time", - "title": "Generated At", + "assessment": { + "$ref": "#/components/schemas/ProductAssessment" + }, + "commercial_approved": { + "default": false, + "title": "Commercial Approved", + "type": "boolean" + }, + "description": { + "maxLength": 5000, + "minLength": 10, + "title": "Description", + "type": "string" + }, + "eligible": { + "title": "Eligible", + "type": "boolean" + }, + "evidence": { + "items": { + "$ref": "#/components/schemas/Evidence" + }, + "maxItems": 10, + "title": "Evidence", + "type": "array" + }, + "excluded": { + "title": "Excluded", + "type": "boolean" + }, + "id": { + "format": "uuid", + "title": "Id", "type": "string" }, - "queues": { - "items": { - "$ref": "#/components/schemas/QueueOut" + "listings": { + "items": { + "$ref": "#/components/schemas/ListingOut" + }, + "title": "Listings", + "type": "array" + }, + "manual_sources": { + "items": { + "$ref": "#/components/schemas/ManualProductSourceOut" + }, + "title": "Manual Sources", + "type": "array" + }, + "metadata_listing_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Metadata Listing Id" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "partnership_type": { + "default": "launch_platform", + "enum": [ + "launch_platform", + "direct" + ], + "title": "Partnership Type", + "type": "string" + }, + "pricing": { + "default": "unknown", + "enum": [ + "free", + "freemium", + "paid", + "unknown" + ], + "title": "Pricing", + "type": "string" + }, + "product_url": { + "title": "Product Url", + "type": "string" + }, + "reviews": { + "items": { + "$ref": "#/components/schemas/PartnerReviewEvent" + }, + "title": "Reviews", + "type": "array" + }, + "revision": { + "title": "Revision", + "type": "integer" + }, + "status": { + "enum": [ + "pending", + "approved", + "rejected", + "paused", + "withdrawn" + ], + "title": "Status", + "type": "string" + }, + "technologies": { + "items": { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + "maxItems": 20, + "title": "Technologies", + "type": "array" + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" + }, + "verified_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Verified At" + } + }, + "required": [ + "name", + "product_url", + "description", + "listings", + "metadata_listing_id", + "assessment", + "excluded", + "id", + "revision", + "status", + "verified_at", + "updated_at", + "reviews", + "eligible" + ], + "title": "ProductOut", + "type": "object" + }, + "ProductPromotionInput": { + "additionalProperties": false, + "properties": { + "ad_formats": { + "anyOf": [ + { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "maxItems": 4, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Ad Formats" + }, + "ad_image_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ad Image Url" + }, + "excluded": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Excluded" + }, + "promotion_surfaces": { + "anyOf": [ + { + "items": { + "enum": [ + "article", + "topic", + "feed" + ], + "type": "string" + }, + "maxItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Promotion Surfaces" + } + }, + "title": "ProductPromotionInput", + "type": "object" + }, + "ProductPromotionOut": { + "additionalProperties": false, + "properties": { + "account_id": { + "format": "uuid", + "title": "Account Id", + "type": "string" + }, + "ad_formats": { + "anyOf": [ + { + "items": { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + "maxItems": 4, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Ad Formats" + }, + "ad_image_status": { + "enum": [ + "default", + "queued", + "running", + "ready", + "failed" + ], + "title": "Ad Image Status", + "type": "string" + }, + "ad_image_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ad Image Url" + }, + "default_image_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Image Url" + }, + "excluded": { + "title": "Excluded", + "type": "boolean" + }, + "image_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image Url" + }, + "product_id": { + "format": "uuid", + "title": "Product Id", + "type": "string" + }, + "promotion_surfaces": { + "anyOf": [ + { + "items": { + "enum": [ + "article", + "topic", + "feed" + ], + "type": "string" + }, + "maxItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Promotion Surfaces" + } + }, + "required": [ + "excluded", + "account_id", + "product_id", + "image_url", + "default_image_url", + "ad_image_status" + ], + "title": "ProductPromotionOut", + "type": "object" + }, + "ProductSnapshot": { + "properties": { + "description": { + "maxLength": 5000, + "minLength": 10, + "title": "Description", + "type": "string" + }, + "evidence": { + "items": { + "$ref": "#/components/schemas/Evidence" + }, + "maxItems": 10, + "title": "Evidence", + "type": "array" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "pricing": { + "default": "unknown", + "enum": [ + "free", + "freemium", + "paid", + "unknown" + ], + "title": "Pricing", + "type": "string" + }, + "product_url": { + "title": "Product Url", + "type": "string" + }, + "revision": { + "title": "Revision", + "type": "integer" + }, + "technologies": { + "items": { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + "maxItems": 20, + "title": "Technologies", + "type": "array" + } + }, + "required": [ + "name", + "product_url", + "description", + "revision" + ], + "title": "ProductSnapshot", + "type": "object" + }, + "ProfileLink": { + "additionalProperties": false, + "properties": { + "label": { + "anyOf": [ + { + "maxLength": 80, + "pattern": "^[^\\x00]*$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label" + }, + "url": { + "maxLength": 2048, + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "ProfileLink", + "type": "object" + }, + "ProfileSettings": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Avatar Url" + }, + "display_name": { + "anyOf": [ + { + "maxLength": 100, + "pattern": "^[^\\x00]*$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name" + } + }, + "title": "ProfileSettings", + "type": "object" + }, + "PublicationDecisionOut": { + "properties": { + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "decision": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Decision", + "type": "object" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + } + }, + "required": [ + "id", + "created_at", + "decision" + ], + "title": "PublicationDecisionOut", + "type": "object" + }, + "PublicationPolicyReviewOut": { + "properties": { + "actor": { + "title": "Actor", + "type": "string" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "mode": { + "title": "Mode", + "type": "string" + }, + "revision": { + "title": "Revision", + "type": "integer" + } + }, + "required": [ + "id", + "created_at", + "mode", + "revision", + "actor" + ], + "title": "PublicationPolicyReviewOut", + "type": "object" + }, + "PublicationPolicyUpdate": { + "additionalProperties": false, + "properties": { + "expected_revision": { + "minimum": 0.0, + "title": "Expected Revision", + "type": "integer" + }, + "mode": { + "enum": [ + "manual", + "preview", + "auto" + ], + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode", + "expected_revision" + ], + "title": "PublicationPolicyUpdate", + "type": "object" + }, + "Quality": { + "additionalProperties": false, + "properties": { + "confidence": { + "maximum": 1.0, + "minimum": 0.0, + "title": "Confidence", + "type": "number" + }, + "entries": { + "items": { + "$ref": "#/components/schemas/EntryQuality" + }, + "maxItems": 10, + "minItems": 1, + "title": "Entries", + "type": "array" + } + }, + "required": [ + "confidence", + "entries" + ], + "title": "Quality", + "type": "object" + }, + "QueueOut": { + "properties": { + "busy_workers": { + "title": "Busy Workers", + "type": "integer" + }, + "dispatched": { + "title": "Dispatched", + "type": "integer" + }, + "failed": { + "default": 0, + "title": "Failed", + "type": "integer" + }, + "idle_workers": { + "title": "Idle Workers", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "oldest_queued_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Oldest Queued At" + }, + "queued": { + "default": 0, + "title": "Queued", + "type": "integer" + }, + "registered_workers": { + "title": "Registered Workers", + "type": "integer" + }, + "review_required": { + "default": 0, + "title": "Review Required", + "type": "integer" + }, + "running": { + "default": 0, + "title": "Running", + "type": "integer" + }, + "succeeded": { + "default": 0, + "title": "Succeeded", + "type": "integer" + }, + "suspended_workers": { + "title": "Suspended Workers", + "type": "integer" + } + }, + "required": [ + "name", + "registered_workers", + "busy_workers", + "idle_workers", + "suspended_workers", + "dispatched" + ], + "title": "QueueOut", + "type": "object" + }, + "RecoveryRequest": { + "additionalProperties": false, + "properties": { + "expected_revision": { + "minimum": 0.0, + "title": "Expected Revision", + "type": "integer" + } + }, + "required": [ + "expected_revision" + ], + "title": "RecoveryRequest", + "type": "object" + }, + "RecoveryResult": { + "properties": { + "decision": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Decision" + }, + "job_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Job Id" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "RecoveryResult", + "type": "object" + }, + "RecoveryTarget": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "kind": { + "enum": [ + "article", + "topic-proposal", + "relationship-proposal" + ], + "title": "Kind", + "type": "string" + }, + "revision": { + "default": 0, + "title": "Revision", + "type": "integer" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "id", + "title", + "kind" + ], + "title": "RecoveryTarget", + "type": "object" + }, + "RelationOut": { + "properties": { + "evidence_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evidence Url" + }, + "related_topic_id": { + "format": "uuid", + "title": "Related Topic Id", + "type": "string" + }, + "relation": { + "enum": [ + "uses_language", + "depends_on", + "implements", + "part_of", + "related_to" + ], + "title": "Relation", + "type": "string" + }, + "topic_id": { + "format": "uuid", + "title": "Topic Id", + "type": "string" + } + }, + "required": [ + "topic_id", + "related_topic_id", + "relation", + "evidence_url" + ], + "title": "RelationOut", + "type": "object" + }, + "RelationWrite": { + "additionalProperties": false, + "properties": { + "evidence_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evidence Url" + }, + "related_topic_id": { + "format": "uuid", + "title": "Related Topic Id", + "type": "string" + }, + "relation": { + "enum": [ + "uses_language", + "depends_on", + "implements", + "part_of", + "related_to" + ], + "title": "Relation", + "type": "string" + }, + "topic_id": { + "format": "uuid", + "title": "Topic Id", + "type": "string" + } + }, + "required": [ + "topic_id", + "related_topic_id", + "relation" + ], + "title": "RelationWrite", + "type": "object" + }, + "RelationshipAnalysisRequest": { + "additionalProperties": false, + "properties": { + "related_topic_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Related Topic Id" + } + }, + "title": "RelationshipAnalysisRequest", + "type": "object" + }, + "RelationshipOut": { + "properties": { + "evidence_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evidence Url" + }, + "proposal": { + "anyOf": [ + { + "$ref": "#/components/schemas/RelationshipProposalOut" + }, + { + "type": "null" + } + ] + }, + "related_topic_id": { + "format": "uuid", + "title": "Related Topic Id", + "type": "string" + }, + "related_topic_name": { + "title": "Related Topic Name", + "type": "string" + }, + "relation": { + "enum": [ + "uses_language", + "depends_on", + "implements", + "part_of", + "related_to" + ], + "title": "Relation", + "type": "string" + }, + "status": { + "enum": [ + "approved", + "pending" + ], + "title": "Status", + "type": "string" + }, + "topic_id": { + "format": "uuid", + "title": "Topic Id", + "type": "string" + }, + "topic_name": { + "title": "Topic Name", + "type": "string" + } + }, + "required": [ + "topic_id", + "related_topic_id", + "topic_name", + "related_topic_name", + "relation", + "evidence_url", + "status", + "proposal" + ], + "title": "RelationshipOut", + "type": "object" + }, + "RelationshipProposalOut": { + "additionalProperties": false, + "properties": { + "approval_blocker": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Approval Blocker" + }, + "can_approve": { + "title": "Can Approve", + "type": "boolean" + }, + "content_hash": { + "title": "Content Hash", + "type": "string" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "created_by": { + "additionalProperties": { + "type": "string" + }, + "title": "Created By", + "type": "object" + }, + "evidence_quote": { + "maxLength": 500, + "minLength": 4, + "title": "Evidence Quote", + "type": "string" + }, + "evidence_title": { + "maxLength": 300, + "minLength": 1, + "title": "Evidence Title", + "type": "string" + }, + "evidence_url": { + "maxLength": 2048, + "title": "Evidence Url", + "type": "string" + }, + "explanation": { + "maxLength": 1000, + "minLength": 1, + "title": "Explanation", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "job_id": { + "format": "uuid", + "title": "Job Id", + "type": "string" + }, + "related_topic_id": { + "format": "uuid", + "title": "Related Topic Id", + "type": "string" + }, + "related_topic_name": { + "title": "Related Topic Name", + "type": "string" + }, + "relation": { + "enum": [ + "uses_language", + "depends_on", + "implements", + "part_of", + "related_to" + ], + "title": "Relation", + "type": "string" + }, + "review_note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Review Note" + }, + "reviewed_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reviewed At" + }, + "reviewed_by": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Reviewed By" + }, + "status": { + "enum": [ + "pending", + "approved", + "rejected" + ], + "title": "Status", + "type": "string" + }, + "topic_id": { + "format": "uuid", + "title": "Topic Id", + "type": "string" + }, + "topic_name": { + "title": "Topic Name", + "type": "string" + } + }, + "required": [ + "topic_id", + "related_topic_id", + "relation", + "explanation", + "evidence_url", + "evidence_title", + "evidence_quote", + "id", + "job_id", + "topic_name", + "related_topic_name", + "status", + "created_at", + "created_by", + "reviewed_at", + "reviewed_by", + "review_note", + "content_hash", + "can_approve", + "approval_blocker" + ], + "title": "RelationshipProposalOut", + "type": "object" + }, + "RelationshipReview": { + "additionalProperties": false, + "properties": { + "decision": { + "enum": [ + "approved", + "rejected" + ], + "title": "Decision", + "type": "string" + }, + "expected_input_hash": { + "pattern": "^[a-f0-9]{64}$", + "title": "Expected Input Hash", + "type": "string" + }, + "note": { + "anyOf": [ + { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" + } + }, + "required": [ + "decision", + "expected_input_hash" + ], + "title": "RelationshipReview", + "type": "object" + }, + "ReviewArticle": { + "additionalProperties": false, + "properties": { + "action": { + "enum": [ + "approve", + "reject", + "publish", + "unpublish" + ], + "title": "Action", + "type": "string" + }, + "expected_revision": { + "minimum": 0.0, + "title": "Expected Revision", + "type": "integer" + }, + "note": { + "anyOf": [ + { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" + } + }, + "required": [ + "action", + "expected_revision" + ], + "title": "ReviewArticle", + "type": "object" + }, + "ReviewSource": { + "additionalProperties": false, + "properties": { + "decision": { + "enum": [ + "approved", + "rejected" + ], + "title": "Decision", + "type": "string" + }, + "note": { + "anyOf": [ + { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" + } + }, + "required": [ + "decision" + ], + "title": "ReviewSource", + "type": "object" + }, + "SearchAnalytics": { + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "flush_interval_seconds": { + "title": "Flush Interval Seconds", + "type": "integer" + }, + "generated_at": { + "format": "date-time", + "title": "Generated At", + "type": "string" + }, + "nohits_queries": { + "items": { + "$ref": "#/components/schemas/SearchAnalyticsQuery" + }, + "title": "Nohits Queries", + "type": "array" + }, + "popular_queries": { + "items": { + "$ref": "#/components/schemas/SearchAnalyticsQuery" + }, + "title": "Popular Queries", + "type": "array" + }, + "rules": { + "items": { + "type": "string" + }, + "title": "Rules", + "type": "array" + } + }, + "required": [ + "generated_at", + "enabled", + "flush_interval_seconds", + "rules", + "popular_queries", + "nohits_queries" + ], + "title": "SearchAnalytics", + "type": "object" + }, + "SearchAnalyticsQuery": { + "properties": { + "count": { + "minimum": 0.0, + "title": "Count", + "type": "integer" + }, + "query": { + "title": "Query", + "type": "string" + } + }, + "required": [ + "query", + "count" + ], + "title": "SearchAnalyticsQuery", + "type": "object" + }, + "SearchSuggestionOut": { + "properties": { + "first_seen_at": { + "format": "date-time", + "title": "First Seen At", + "type": "string" + }, + "last_seen_at": { + "format": "date-time", + "title": "Last Seen At", + "type": "string" + }, + "query": { + "title": "Query", + "type": "string" + }, + "query_hash": { + "title": "Query Hash", + "type": "string" + }, + "review_note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Review Note" + }, + "reviewed_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reviewed At" + }, + "status": { + "title": "Status", + "type": "string" + }, + "successful_count": { + "title": "Successful Count", + "type": "integer" + } + }, + "required": [ + "query_hash", + "query", + "successful_count", + "status", + "first_seen_at", + "last_seen_at", + "reviewed_at", + "review_note" + ], + "title": "SearchSuggestionOut", + "type": "object" + }, + "SearchSuggestionReview": { + "properties": { + "note": { + "anyOf": [ + { + "maxLength": 1000, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" + }, + "status": { + "enum": [ + "approved", + "rejected" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "SearchSuggestionReview", + "type": "object" + }, + "SourceImportRequest": { + "additionalProperties": false, + "properties": { + "content": { + "anyOf": [ + { + "maxLength": 1000000, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "format": { + "enum": [ + "opml", + "urls", + "json", + "markdown" + ], + "title": "Format", + "type": "string" + }, + "name": { + "default": "Admin import", + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + } + }, + "required": [ + "format" + ], + "title": "SourceImportRequest", + "type": "object" + }, + "SourceImportResult": { + "properties": { + "created": { + "title": "Created", + "type": "integer" + }, + "existing": { + "title": "Existing", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "created", + "existing", + "total" + ], + "title": "SourceImportResult", + "type": "object" + }, + "SourceOut": { + "properties": { + "approval_status": { + "enum": [ + "pending", + "approved", + "rejected" + ], + "title": "Approval Status", + "type": "string" + }, + "consecutive_failures": { + "title": "Consecutive Failures", + "type": "integer" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "feed_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Feed Url" + }, + "full_automation": { + "title": "Full Automation", + "type": "boolean" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "image_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image Url" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + }, + "last_attempt_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Attempt At" + }, + "last_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Error" + }, + "last_success_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Success At" + }, + "logo_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logo Url" + }, + "logo_variants": { + "items": { + "$ref": "#/components/schemas/ImageVariant" + }, + "title": "Logo Variants", + "type": "array" + }, + "metadata_enriched_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Metadata Enriched At" + }, + "metadata_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Metadata Error" + }, + "name": { + "title": "Name", + "type": "string" + }, + "next_fetch_at": { + "format": "date-time", + "title": "Next Fetch At", + "type": "string" + }, + "poll_interval_seconds": { + "title": "Poll Interval Seconds", + "type": "integer" + }, + "publication_policy": { + "default": "manual", + "enum": [ + "manual", + "preview", + "auto" + ], + "title": "Publication Policy", + "type": "string" + }, + "publication_policy_revision": { + "default": 0, + "title": "Publication Policy Revision", + "type": "integer" + }, + "relevance_assessment": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Relevance Assessment", + "type": "object" + }, + "review_note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Review Note" + }, + "reviewed_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reviewed At" + }, + "reviewed_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reviewed By" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "source_type": { + "$ref": "#/components/schemas/SourceType" + }, + "submission_channel": { + "title": "Submission Channel", + "type": "string" + }, + "submitted_by": { + "anyOf": [ + { + "$ref": "#/components/schemas/SourceSubmitterOut" + }, + { + "type": "null" + } + ] + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" + }, + "website_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Website Url" + } + }, + "required": [ + "id", + "slug", + "name", + "source_type", + "approval_status", + "submitted_by", + "submission_channel", + "reviewed_at", + "reviewed_by", + "review_note", + "metadata_enriched_at", + "metadata_error", + "updated_at", + "feed_url", + "enabled", + "poll_interval_seconds", + "next_fetch_at", + "last_attempt_at", + "last_success_at", + "consecutive_failures", + "last_error", + "created_at" + ], + "title": "SourceOut", + "type": "object" + }, + "SourcePatch": { + "additionalProperties": false, + "properties": { + "description": { + "anyOf": [ + { + "maxLength": 500, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "image_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image Url" + }, + "language": { + "anyOf": [ + { + "maxLength": 35, + "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + }, + "logo_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logo Url" + }, + "name": { + "anyOf": [ + { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "poll_interval_seconds": { + "anyOf": [ + { + "maximum": 604800.0, + "minimum": 300.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Poll Interval Seconds" + }, + "website_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Website Url" + } + }, + "title": "SourcePatch", + "type": "object" + }, + "SourcePreviewOut": { + "additionalProperties": false, + "properties": { + "description": { + "anyOf": [ + { + "maxLength": 500, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "entries_seen": { + "title": "Entries Seen", + "type": "integer" + }, + "entries_skipped": { + "title": "Entries Skipped", + "type": "integer" + }, + "image_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image Url" + }, + "language": { + "anyOf": [ + { + "maxLength": 35, + "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + }, + "logo_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logo Url" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + }, + "website_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Website Url" + } + }, + "required": [ + "name", + "entries_seen", + "entries_skipped", + "warnings" + ], + "title": "SourcePreviewOut", + "type": "object" + }, + "SourcePreviewRequest": { + "additionalProperties": false, + "properties": { + "feed_url": { + "maxLength": 2048, + "title": "Feed Url", + "type": "string" + }, + "source_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" + }, + "source_type": { + "$ref": "#/components/schemas/SourceType" + }, + "use_solver": { + "default": false, + "title": "Use Solver", + "type": "boolean" + } + }, + "required": [ + "feed_url", + "source_type" + ], + "title": "SourcePreviewRequest", + "type": "object" + }, + "SourceRef": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "image_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image Url" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + }, + "logo_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logo Url" + }, + "logo_variants": { + "items": { + "$ref": "#/components/schemas/ImageVariant" + }, + "title": "Logo Variants", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "source_type": { + "$ref": "#/components/schemas/SourceType" + }, + "website_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Website Url" + } + }, + "required": [ + "id", + "slug", + "name", + "source_type" + ], + "title": "SourceRef", + "type": "object" + }, + "SourceReviewOut": { + "properties": { + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "decision": { + "title": "Decision", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" + }, + "source_id": { + "format": "uuid", + "title": "Source Id", + "type": "string" + } + }, + "required": [ + "id", + "source_id", + "decision", + "actor", + "note", + "created_at" + ], + "title": "SourceReviewOut", + "type": "object" + }, + "SourceSubmitter": { + "additionalProperties": false, + "properties": { + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "profile_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Profile Url" + } + }, + "required": [ + "name" + ], + "title": "SourceSubmitter", + "type": "object" + }, + "SourceSubmitterOut": { + "additionalProperties": false, + "properties": { + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "profile_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Profile Url" + }, + "user_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id" + }, + "verified": { + "default": false, + "title": "Verified", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "title": "SourceSubmitterOut", + "type": "object" + }, + "SourceType": { + "enum": [ + "publisher", + "aggregator" + ], + "title": "SourceType", + "type": "string" + }, + "SponsorArticlesOut": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ArticleOut" + }, + "title": "Items", + "type": "array" + }, + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "items", + "total" + ], + "title": "SponsorArticlesOut", + "type": "object" + }, + "TableSettings": { + "additionalProperties": false, + "properties": { + "columns": { + "maxProperties": 100, + "patternProperties": { + "^[a-zA-Z0-9_-]{1,80}$": { + "type": "boolean" + } + }, + "title": "Columns", + "type": "object" + }, + "query": { + "maxProperties": 30, + "patternProperties": { + "^[a-zA-Z0-9_-]{1,80}$": { + "maxLength": 500, + "type": "string" + } + }, + "title": "Query", + "type": "object" + } + }, + "title": "TableSettings", + "type": "object" + }, + "TableSettingsPatch": { + "additionalProperties": false, + "properties": { + "columns": { + "anyOf": [ + { + "maxProperties": 100, + "patternProperties": { + "^[a-zA-Z0-9_-]{1,80}$": { + "type": "boolean" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Columns" + }, + "query": { + "anyOf": [ + { + "maxProperties": 30, + "patternProperties": { + "^[a-zA-Z0-9_-]{1,80}$": { + "maxLength": 500, + "type": "string" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Query" + } + }, + "title": "TableSettingsPatch", + "type": "object" + }, + "TagOut": { + "properties": { + "aliases": { + "items": { + "type": "string" + }, + "title": "Aliases", + "type": "array" + }, + "auto_link_topic": { + "default": true, + "title": "Auto Link Topic", + "type": "boolean" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "topic_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + }, + "topic_match_checked_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Match Checked At" + }, + "topic_match_status": { + "default": "pending", + "title": "Topic Match Status", + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug", + "aliases" + ], + "title": "TagOut", + "type": "object" + }, + "TagPatch": { + "additionalProperties": false, + "properties": { + "aliases": { + "anyOf": [ + { + "items": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "maxItems": 100, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Aliases" + }, + "auto_link_topic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Auto Link Topic" + }, + "name": { + "anyOf": [ + { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "slug": { + "anyOf": [ + { + "maxLength": 100, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Slug" + }, + "topic_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + "title": "TagPatch", + "type": "object" + }, + "TagWrite": { + "additionalProperties": false, + "properties": { + "aliases": { + "items": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "maxItems": 100, + "title": "Aliases", + "type": "array" + }, + "auto_link_topic": { + "default": true, + "title": "Auto Link Topic", + "type": "boolean" + }, + "name": { + "maxLength": 100, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "slug": { + "maxLength": 100, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "title": "Slug", + "type": "string" + }, + "topic_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + "required": [ + "name", + "slug" + ], + "title": "TagWrite", + "type": "object" + }, + "TopicAnalysisBatchOut": { + "additionalProperties": false, + "properties": { + "already_active": { + "title": "Already Active", + "type": "integer" + }, + "complete": { + "title": "Complete", + "type": "integer" + }, + "pending": { + "title": "Pending", + "type": "integer" + }, + "queued": { + "title": "Queued", + "type": "integer" + } + }, + "required": [ + "queued", + "already_active", + "complete", + "pending" + ], + "title": "TopicAnalysisBatchOut", + "type": "object" + }, + "TopicAnalysisOut": { + "additionalProperties": false, + "properties": { + "attempts": { + "title": "Attempts", + "type": "integer" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + }, + "finished_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Finished At" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Outcome" + }, + "reasons": { + "items": { + "type": "string" + }, + "title": "Reasons", + "type": "array" + }, + "status": { + "enum": [ + "queued", + "running", + "succeeded", + "failed" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "id", + "status", + "attempts", + "model", + "created_at", + "finished_at", + "outcome", + "error" + ], + "title": "TopicAnalysisOut", + "type": "object" + }, + "TopicDecisionDay": { + "properties": { + "approved": { + "default": 0, + "title": "Approved", + "type": "integer" + }, + "calls": { + "default": 0, + "title": "Calls", + "type": "integer" + }, + "date": { + "title": "Date", + "type": "string" + }, + "deferred": { + "default": 0, + "title": "Deferred", + "type": "integer" + }, + "escalations": { + "default": 0, + "title": "Escalations", + "type": "integer" + }, + "rejected": { + "default": 0, + "title": "Rejected", + "type": "integer" + }, + "repeated_stages": { + "default": 0, + "title": "Repeated Stages", + "type": "integer" + } + }, + "required": [ + "date" + ], + "title": "TopicDecisionDay", + "type": "object" + }, + "TopicDecisionMetrics": { + "properties": { + "actionable": { + "default": 0, + "title": "Actionable", + "type": "integer" + }, + "approved": { + "default": 0, + "title": "Approved", + "type": "integer" + }, + "awaiting_review": { + "default": 0, + "title": "Awaiting Review", + "type": "integer" + }, + "calls": { + "default": 0, + "title": "Calls", + "type": "integer" + }, + "charged_tokens": { + "default": 0, + "title": "Charged Tokens", + "type": "integer" + }, + "decisions_per_hour": { + "default": 0, + "title": "Decisions Per Hour", + "type": "number" + }, + "deferred": { + "default": 0, + "title": "Deferred", + "type": "integer" + }, + "deferred_reasons": { + "additionalProperties": { + "type": "integer" + }, + "title": "Deferred Reasons", + "type": "object" + }, + "escalation_percent": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Escalation Percent" + }, + "escalations": { + "default": 0, + "title": "Escalations", + "type": "integer" + }, + "estimated_drain_hours": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Estimated Drain Hours" + }, + "pending": { + "default": 0, + "title": "Pending", + "type": "integer" + }, + "rejected": { + "default": 0, + "title": "Rejected", + "type": "integer" + }, + "repeat_calls": { + "default": 0, + "title": "Repeat Calls", + "type": "integer" + }, + "tokens": { + "default": 0, + "title": "Tokens", + "type": "integer" + }, + "tokens_per_completed_topic": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Tokens Per Completed Topic" + }, + "unreported_calls": { + "default": 0, + "title": "Unreported Calls", + "type": "integer" + } + }, + "title": "TopicDecisionMetrics", + "type": "object" + }, + "TopicDeleteImpact": { + "properties": { + "articles": { + "title": "Articles", + "type": "integer" + }, + "pending_topic_proposals": { + "title": "Pending Topic Proposals", + "type": "integer" + }, + "published_articles": { + "title": "Published Articles", + "type": "integer" + }, + "relationship_proposals": { + "title": "Relationship Proposals", + "type": "integer" + }, + "relationships": { + "title": "Relationships", + "type": "integer" + }, + "research_jobs": { + "title": "Research Jobs", + "type": "integer" + }, + "tags": { + "title": "Tags", + "type": "integer" + } + }, + "required": [ + "articles", + "published_articles", + "tags", + "relationships", + "relationship_proposals", + "research_jobs", + "pending_topic_proposals" + ], + "title": "TopicDeleteImpact", + "type": "object" + }, + "TopicDraft": { + "additionalProperties": false, + "description": "Reviewed topic fields; lifecycle status cannot be supplied by imports.", + "properties": { + "aliases": { + "items": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "maxItems": 50, + "title": "Aliases", + "type": "array" + }, + "description": { + "anyOf": [ + { + "maxLength": 2000, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "facts": { + "items": { + "$ref": "#/components/schemas/TopicFact" + }, + "maxItems": 50, + "title": "Facts", + "type": "array" + }, + "keywords": { + "items": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "maxItems": 100, + "title": "Keywords", + "type": "array" + }, + "kind": { + "enum": [ + "benchmark", + "competition", + "concept", + "dataset", + "database", + "discipline", + "event", + "format", + "framework", + "game", + "game_engine", + "hardware", + "language", + "library", + "license", + "model", + "operating_system", + "organization", + "package", + "platform", + "practice", + "product", + "program", + "protocol", + "publication", + "runtime", + "service", + "software", + "standard", + "technology", + "tool", + "unclassified" + ], + "title": "Kind", + "type": "string" + }, + "logo_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Logo Url" + }, + "name": { + "maxLength": 100, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "slug": { + "maxLength": 100, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "title": "Slug", + "type": "string" + }, + "website_url": { + "anyOf": [ + { + "maxLength": 2048, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Website Url" + } + }, + "required": [ + "name", + "slug", + "kind" + ], + "title": "TopicDraft", + "type": "object" + }, + "TopicEnrichmentPreview": { + "additionalProperties": false, + "properties": { + "articles_examined": { + "title": "Articles Examined", + "type": "integer" + }, + "preview_token": { + "title": "Preview Token", + "type": "string" + }, + "suggestions": { + "items": { + "$ref": "#/components/schemas/KeywordSuggestion" + }, + "title": "Suggestions", + "type": "array" + }, + "topic": { + "$ref": "#/components/schemas/TopicDraft" + } + }, + "required": [ + "topic", + "preview_token", + "articles_examined", + "suggestions" + ], + "title": "TopicEnrichmentPreview", + "type": "object" + }, + "TopicEnrichmentSubmit": { + "additionalProperties": false, + "properties": { + "keywords": { + "items": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "maxItems": 25, + "minItems": 1, + "title": "Keywords", + "type": "array" + }, + "preview_token": { + "pattern": "^[a-f0-9]{64}$", + "title": "Preview Token", + "type": "string" + } + }, + "required": [ + "preview_token", + "keywords" + ], + "title": "TopicEnrichmentSubmit", + "type": "object" + }, + "TopicFact": { + "additionalProperties": false, + "properties": { + "name": { + "maxLength": 100, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "retrieved_at": { + "format": "date-time", + "title": "Retrieved At", + "type": "string" + }, + "source_url": { + "maxLength": 2048, + "title": "Source Url", + "type": "string" + }, + "value": { + "maxLength": 500, + "minLength": 1, + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value", + "source_url", + "retrieved_at" + ], + "title": "TopicFact", + "type": "object" + }, + "TopicImport": { + "additionalProperties": false, + "properties": { + "content": { + "maxLength": 262144, + "minLength": 1, + "title": "Content", + "type": "string" + }, + "format": { + "enum": [ + "json", + "csv" + ], + "title": "Format", + "type": "string" + }, + "source_name": { + "maxLength": 200, + "minLength": 1, + "title": "Source Name", + "type": "string" + } + }, + "required": [ + "format", + "content", + "source_name" + ], + "title": "TopicImport", + "type": "object" + }, + "TopicImportPreview": { + "additionalProperties": false, + "properties": { + "can_submit": { + "title": "Can Submit", + "type": "boolean" + }, + "preview_token": { + "title": "Preview Token", + "type": "string" + }, + "rows": { + "items": { + "$ref": "#/components/schemas/TopicImportRow" + }, + "title": "Rows", + "type": "array" + } + }, + "required": [ + "rows", + "preview_token", + "can_submit" + ], + "title": "TopicImportPreview", + "type": "object" + }, + "TopicImportRow": { + "additionalProperties": false, + "properties": { + "action": { + "enum": [ + "create", + "update", + "unchanged", + "invalid" + ], + "title": "Action", + "type": "string" + }, + "issues": { + "items": { + "type": "string" + }, + "title": "Issues", + "type": "array" + }, + "row": { + "title": "Row", + "type": "integer" + }, + "topic": { + "anyOf": [ + { + "$ref": "#/components/schemas/TopicDraft" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "row", + "action" + ], + "title": "TopicImportRow", + "type": "object" + }, + "TopicImportSubmit": { + "additionalProperties": false, + "properties": { + "content": { + "maxLength": 262144, + "minLength": 1, + "title": "Content", + "type": "string" + }, + "format": { + "enum": [ + "json", + "csv" + ], + "title": "Format", + "type": "string" + }, + "preview_token": { + "pattern": "^[a-f0-9]{64}$", + "title": "Preview Token", + "type": "string" + }, + "source_name": { + "maxLength": 200, + "minLength": 1, + "title": "Source Name", + "type": "string" + } + }, + "required": [ + "format", + "content", + "source_name", + "preview_token" + ], + "title": "TopicImportSubmit", + "type": "object" + }, + "TopicProposalFilterOptions": { + "properties": { + "kinds": { + "items": { + "type": "string" + }, + "title": "Kinds", + "type": "array" + }, + "sources": { + "items": { + "type": "string" + }, + "title": "Sources", + "type": "array" + } + }, + "required": [ + "kinds", + "sources" + ], + "title": "TopicProposalFilterOptions", + "type": "object" + }, + "TopicProposalOut": { + "additionalProperties": false, + "properties": { + "action": { + "enum": [ + "create", + "update" + ], + "title": "Action", + "type": "string" + }, + "analysis": { + "anyOf": [ + { + "$ref": "#/components/schemas/TopicAnalysisOut" + }, + { + "type": "null" + } + ] + }, + "applied": { + "anyOf": [ + { + "$ref": "#/components/schemas/TopicDraft" + }, + { + "type": "null" + } + ] + }, + "batch_id": { + "format": "uuid", + "title": "Batch Id", + "type": "string" + }, + "before": { + "anyOf": [ + { + "$ref": "#/components/schemas/TopicDraft" + }, + { + "type": "null" + } + ] + }, + "content_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content Hash" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "created_by": { + "additionalProperties": { + "type": "string" + }, + "title": "Created By", + "type": "object" + }, + "evidence": { + "items": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "title": "Evidence", + "type": "array" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "origin": { + "enum": [ + "import", + "article_enrichment", + "ai_analysis" + ], + "title": "Origin", + "type": "string" + }, + "proposed": { + "$ref": "#/components/schemas/TopicDraft" + }, + "review_note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Review Note" + }, + "reviewed_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reviewed At" + }, + "reviewed_by": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Reviewed By" + }, + "source_name": { + "title": "Source Name", + "type": "string" + }, + "status": { + "enum": [ + "pending", + "approved", + "rejected" + ], + "title": "Status", + "type": "string" + }, + "topic_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + "required": [ + "id", + "batch_id", + "topic_id", + "action", + "origin", + "source_name", + "proposed", + "before", + "evidence", + "status", + "created_at", + "created_by", + "reviewed_at", + "reviewed_by", + "review_note", + "applied" + ], + "title": "TopicProposalOut", + "type": "object" + }, + "TopicReplacementOut": { + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "proposal_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proposal Id" + }, + "slug": { + "title": "Slug", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "topic_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + "required": [ + "id", + "name", + "slug", + "kind", + "status", + "topic_id", + "proposal_id" + ], + "title": "TopicReplacementOut", + "type": "object" + }, + "TopicReview": { + "additionalProperties": false, + "properties": { + "decision": { + "enum": [ + "approved", + "rejected" + ], + "title": "Decision", + "type": "string" + }, + "expected_input_hash": { + "anyOf": [ + { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Expected Input Hash" + }, + "note": { + "anyOf": [ + { + "maxLength": 1000, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Note" + }, + "topic": { + "anyOf": [ + { + "$ref": "#/components/schemas/TopicDraft" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "decision" + ], + "title": "TopicReview", + "type": "object" + }, + "TopicSelection": { + "additionalProperties": false, + "properties": { + "evidence": { + "maxLength": 500, + "minLength": 4, + "title": "Evidence", + "type": "string" + }, + "relevance": { + "maximum": 1.0, + "minimum": 0.0, + "title": "Relevance", + "type": "number" + }, + "role": { + "enum": [ + "primary", + "supporting", + "comparison", + "incidental" + ], + "title": "Role", + "type": "string" + }, + "topic_id": { + "format": "uuid", + "title": "Topic Id", + "type": "string" + } + }, + "required": [ + "topic_id", + "role", + "relevance", + "evidence" + ], + "title": "TopicSelection", + "type": "object" + }, + "Totals": { + "properties": { + "clicks": { + "title": "Clicks", + "type": "integer" + }, + "ctr": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Ctr" + }, + "impressions": { + "title": "Impressions", + "type": "integer" + }, + "measured_days": { + "title": "Measured Days", + "type": "integer" + } + }, + "required": [ + "impressions", + "clicks", + "ctr", + "measured_days" + ], + "title": "Totals", + "type": "object" + }, + "Trend": { + "properties": { + "clicks": { + "title": "Clicks", + "type": "integer" + }, + "day": { + "format": "date", + "title": "Day", + "type": "string" + }, + "impressions": { + "title": "Impressions", + "type": "integer" + } + }, + "required": [ + "day", + "impressions", + "clicks" + ], + "title": "Trend", + "type": "object" + }, + "UnhealthyResponse": { + "properties": { + "status": { + "enum": [ + "migration_required", + "unavailable" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "UnhealthyResponse", + "type": "object" + }, + "UserAppearanceSettings": { + "additionalProperties": false, + "properties": { + "date_format": { + "default": "locale", + "enum": [ + "locale", + "iso", + "day-first", + "month-first" + ], + "title": "Date Format", + "type": "string" + }, + "theme": { + "default": "system", + "enum": [ + "system", + "light", + "dark" + ], + "title": "Theme", + "type": "string" + }, + "time_format": { + "default": "system", + "enum": [ + "system", + "12", + "24" + ], + "title": "Time Format", + "type": "string" + }, + "timezone": { + "default": "local", + "maxLength": 100, + "title": "Timezone", + "type": "string" + } + }, + "title": "UserAppearanceSettings", + "type": "object" + }, + "UserReadingStreak": { + "additionalProperties": false, + "properties": { + "current_days": { + "default": 0, + "title": "Current Days", + "type": "integer" + }, + "last_read_date": { + "anyOf": [ + { + "format": "date", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Read Date" + }, + "longest_days": { + "default": 0, + "title": "Longest Days", + "type": "integer" + }, + "total_days": { + "default": 0, + "title": "Total Days", + "type": "integer" + } + }, + "title": "UserReadingStreak", + "type": "object" + }, + "UserSettings": { + "additionalProperties": false, + "properties": { + "appearance": { + "$ref": "#/components/schemas/AppearanceSettings" + }, + "defaults": { + "$ref": "#/components/schemas/DefaultSettings" + }, + "notifications": { + "$ref": "#/components/schemas/NotificationSettings" + }, + "profile": { + "$ref": "#/components/schemas/ProfileSettings" + }, + "tables": { + "additionalProperties": { + "$ref": "#/components/schemas/TableSettings" + }, + "title": "Tables", + "type": "object" + } + }, + "title": "UserSettings", + "type": "object" + }, + "ValidationIssue": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "title": "Loc", + "type": "array" + }, + "msg": { + "title": "Msg", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationIssue", + "type": "object" + }, + "WorkerJob": { + "properties": { + "article_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Article Id" + }, + "elapsed_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Elapsed Seconds" + }, + "id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Kind" + }, + "outcome": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Outcome" + }, + "proposal_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proposal Id" + }, + "rq_id": { + "title": "Rq Id", + "type": "string" + }, + "source_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" + }, + "started_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Started At" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "target_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Target Name" + }, + "topic_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + "required": [ + "rq_id" + ], + "title": "WorkerJob", + "type": "object" + }, + "WorkerOut": { + "properties": { + "completed_executions": { + "title": "Completed Executions", + "type": "integer" + }, + "current_job": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkerJob" + }, + { + "type": "null" + } + ] + }, + "failed_executions": { + "title": "Failed Executions", + "type": "integer" + }, + "heartbeat_age_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Heartbeat Age Seconds" + }, + "hostname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Hostname" + }, + "last_heartbeat": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Heartbeat" + }, + "name": { + "title": "Name", + "type": "string" + }, + "pid": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Pid" + }, + "queues": { + "items": { + "type": "string" + }, + "title": "Queues", + "type": "array" + }, + "registered": { + "title": "Registered", + "type": "boolean" + }, + "registration_ttl_seconds": { + "title": "Registration Ttl Seconds", + "type": "integer" + }, + "role": { + "enum": [ + "ai", + "background", + "mixed", + "solver" + ], + "title": "Role", + "type": "string" + }, + "started_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Started At" + }, + "state": { + "title": "State", + "type": "string" + }, + "uptime_seconds": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Uptime Seconds" + }, + "working_seconds": { + "title": "Working Seconds", + "type": "number" + } + }, + "required": [ + "name", + "role", + "queues", + "state", + "registered", + "hostname", + "pid", + "started_at", + "last_heartbeat", + "uptime_seconds", + "heartbeat_age_seconds", + "registration_ttl_seconds", + "completed_executions", + "failed_executions", + "working_seconds" + ], + "title": "WorkerOut", + "type": "object" + }, + "WorkersSnapshot": { + "properties": { + "ai_cooldown_seconds": { + "title": "Ai Cooldown Seconds", + "type": "integer" + }, + "generated_at": { + "format": "date-time", + "title": "Generated At", + "type": "string" + }, + "queues": { + "items": { + "$ref": "#/components/schemas/QueueOut" + }, + "title": "Queues", + "type": "array" + }, + "workers": { + "items": { + "$ref": "#/components/schemas/WorkerOut" + }, + "title": "Workers", + "type": "array" + } + }, + "required": [ + "generated_at", + "workers", + "queues", + "ai_cooldown_seconds" + ], + "title": "WorkersSnapshot", + "type": "object" + }, + "devfeed_admin_api__articles__ArticleTopicOut": { + "properties": { + "evidence": { + "title": "Evidence", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "origin": { + "title": "Origin", + "type": "string" + }, + "relevance": { + "title": "Relevance", + "type": "number" + }, + "role": { + "title": "Role", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "topic_id": { + "format": "uuid", + "title": "Topic Id", + "type": "string" + } + }, + "required": [ + "topic_id", + "name", + "status", + "role", + "relevance", + "evidence", + "origin" + ], + "title": "ArticleTopicOut", + "type": "object" + }, + "devfeed_core__schemas__ArticleTopicOut": { + "properties": { + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relevance": { + "title": "Relevance", + "type": "number" + }, + "role": { + "title": "Role", + "type": "string" + }, + "slug": { + "title": "Slug", + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug", + "kind", + "role", + "relevance" + ], + "title": "ArticleTopicOut", + "type": "object" + } + }, + "securitySchemes": { + "APIKeyCookie": { + "description": "Admin session cookie (devfeed_admin_session in HTTP development).", + "in": "cookie", + "name": "__Host-devfeed_admin_session", + "type": "apiKey" + } + } + }, + "info": { + "description": "Private administration API. OIDC sessions and CSRF protection required.", + "title": "DevFeed Admin API", + "version": "0.0.52" + }, + "openapi": "3.1.0", + "paths": { + "/health/live": { + "get": { + "operationId": "live_health_live_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "summary": "Live", + "tags": [ + "health" + ] + } + }, + "/health/ready": { + "get": { + "operationId": "ready_health_ready_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnhealthyResponse" + } + } + }, + "description": "Not ready" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "summary": "Ready", + "tags": [ + "health" + ] + } + }, + "/v1/admin/ai/connection": { + "get": { + "operationId": "admin_ai_connection", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CodexStatus" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Status", + "tags": [ + "admin-ai-connection" + ] + } + }, + "/v1/admin/ai/connection/login": { + "post": { + "operationId": "admin_ai_login", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceLogin" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Login", + "tags": [ + "admin-ai-connection" + ] + } + }, + "/v1/admin/ai/connection/login/cancel": { + "post": { + "operationId": "admin_ai_login_cancel", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelLogin" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Cancel", + "tags": [ + "admin-ai-connection" + ] + } + }, + "/v1/admin/articles": { + "get": { + "operationId": "admin_articles_list", + "parameters": [ + { + "in": "query", + "name": "review_status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "pending", + "approved", + "rejected" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Review Status" + } + }, + { + "in": "query", + "name": "publication_status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "unpublished", + "published" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Publication Status" + } + }, + { + "in": "query", + "name": "source_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" + } + }, + { + "in": "query", + "name": "topic_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + { + "in": "query", + "name": "tag_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tag Id" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_AdminArticleOut_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Articles", + "tags": [ + "admin-articles" + ] + }, + "post": { + "operationId": "admin_article_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Create", + "tags": [ + "admin-articles" + ] + } + }, + "/v1/admin/articles/{article_id}": { + "delete": { + "operationId": "admin_article_delete", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Remove", + "tags": [ + "admin-articles" + ] + }, + "get": { + "operationId": "admin_article_get", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Detail", + "tags": [ + "admin-articles" + ] + }, + "put": { + "operationId": "admin_article_update", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Update", + "tags": [ + "admin-articles" + ] + } + }, + "/v1/admin/articles/{article_id}/classify": { + "post": { + "operationId": "admin_article_classify", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClassifyArticle" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Classify", + "tags": [ + "admin-articles" + ] + } + }, + "/v1/admin/articles/{article_id}/content": { + "get": { + "operationId": "admin_article_content", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ArticleContentOut" + }, + { + "type": "null" + } + ], + "title": "Response Admin Article Content" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Content", + "tags": [ + "admin-articles" + ] + } + }, + "/v1/admin/articles/{article_id}/review": { + "post": { + "operationId": "admin_article_review", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewArticle" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Review", + "tags": [ + "admin-articles" + ] + } + }, + "/v1/admin/articles/{article_id}/reviews": { + "get": { + "operationId": "admin_article_reviews", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_ArticleReviewOut_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Reviews", + "tags": [ + "admin-articles" + ] + } + }, + "/v1/admin/auth/callback": { + "get": { + "operationId": "admin_auth_callback", + "parameters": [ + { + "in": "query", + "name": "state", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "State" + } + }, + { + "in": "query", + "name": "code", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + } + }, + { + "in": "query", + "name": "error", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + { + "in": "query", + "name": "iss", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Iss" + } + } + ], + "responses": { + "302": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "summary": "Callback", + "tags": [ + "admin-auth" + ] + } + }, + "/v1/admin/auth/config": { + "get": { + "operationId": "admin_auth_config", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthConfig" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "summary": "Config", + "tags": [ + "admin-auth" + ] + } + }, + "/v1/admin/auth/login": { + "get": { + "operationId": "admin_auth_login", + "parameters": [ + { + "in": "query", + "name": "reauthenticate", + "required": false, + "schema": { + "default": false, + "title": "Reauthenticate", + "type": "boolean" + } + }, + { + "in": "query", + "name": "provider", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "github", + "google" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" + } + } + ], + "responses": { + "302": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "summary": "Login", + "tags": [ + "admin-auth" + ] + } + }, + "/v1/admin/auth/logout": { + "post": { + "operationId": "admin_auth_logout", + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Logout", + "tags": [ + "admin-auth" + ] + } + }, + "/v1/admin/auth/me": { + "get": { + "operationId": "admin_auth_me", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminIdentity" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Me", + "tags": [ + "admin-auth" + ] + } + }, + "/v1/admin/automation/articles/{article_id}/decisions": { + "get": { + "operationId": "admin_publication_decisions", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_PublicationDecisionOut_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Publication Decisions", + "tags": [ + "admin-automation" + ] + } + }, + "/v1/admin/automation/articles/{article_id}/{action}": { + "post": { + "operationId": "admin_automation_recover", + "parameters": [ + { + "in": "path", + "name": "article_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Article Id", + "type": "string" + } + }, + { + "in": "path", + "name": "action", + "required": true, + "schema": { + "enum": [ + "enrich", + "analyze", + "evaluate" + ], + "title": "Action", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecoveryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecoveryResult" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Recover", + "tags": [ + "admin-automation" + ] + } + }, + "/v1/admin/automation/sources/{source_id}/policies": { + "get": { + "operationId": "admin_publication_policy_history", + "parameters": [ + { + "in": "path", + "name": "source_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Source Id", + "type": "string" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_PublicationPolicyReviewOut_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Publication Policy History", + "tags": [ + "admin-automation" + ] + } + }, + "/v1/admin/ingestion/article-jobs": { + "get": { + "operationId": "article_jobs_v1_admin_ingestion_article_jobs_get", + "parameters": [ + { + "in": "query", + "name": "article_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Article Id" + } + }, + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "queued", + "running", + "succeeded", + "failed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ArticleEnrichmentJobOut" + }, + "title": "Response Article Jobs V1 Admin Ingestion Article Jobs Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Article Jobs", + "tags": [ + "admin-ingestion" + ] + } + }, + "/v1/admin/ingestion/article-jobs/{job_id}": { + "get": { + "operationId": "article_job_detail_v1_admin_ingestion_article_jobs__job_id__get", + "parameters": [ + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArticleEnrichmentJobOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Article Job Detail", + "tags": [ + "admin-ingestion" + ] + } + }, + "/v1/admin/ingestion/jobs": { + "get": { + "operationId": "jobs_v1_admin_ingestion_jobs_get", + "parameters": [ + { + "in": "query", + "name": "source_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" + } + }, + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "queued", + "running", + "succeeded", + "failed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/JobOut" + }, + "title": "Response Jobs V1 Admin Ingestion Jobs Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Jobs", + "tags": [ + "admin-ingestion" + ] + } + }, + "/v1/admin/ingestion/jobs/{job_id}": { + "get": { + "operationId": "job_detail_v1_admin_ingestion_jobs__job_id__get", + "parameters": [ + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Job Detail", + "tags": [ + "admin-ingestion" + ] + } + }, + "/v1/admin/ingestion/status": { + "get": { + "operationId": "status_v1_admin_ingestion_status_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IngestionStatus" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Status", + "tags": [ + "admin-ingestion" + ] + } + }, + "/v1/admin/jobs/ai-analysis": { + "get": { + "description": "Page article and topic research runs together before loading their details.", + "operationId": "admin_ai_analysis_jobs_list", + "parameters": [ + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "queued", + "running", + "succeeded", + "failed", + "retried" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "in": "query", + "name": "analysis_type", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "articles", + "topics" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Analysis Type" + } + }, + { + "in": "query", + "name": "article_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Article Id" + } + }, + { + "in": "query", + "name": "proposal_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proposal Id" + } + }, + { + "in": "query", + "name": "topic_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + { + "in": "query", + "name": "retryable_only", + "required": false, + "schema": { + "default": false, + "title": "Retryable Only", + "type": "boolean" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_AdminJobOut_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Ai Analysis Jobs", + "tags": [ + "admin-jobs" + ] + } + }, + "/v1/admin/jobs/{kind}": { + "get": { + "operationId": "admin_jobs_list", + "parameters": [ + { + "in": "path", + "name": "kind", + "required": true, + "schema": { + "enum": [ + "ingestion", + "source-discovery", + "partner-evaluation", + "partner-pipeline", + "article-enrichment", + "images", + "source-enrichment", + "analysis", + "topic-analysis", + "notifications" + ], + "title": "Kind", + "type": "string" + } + }, + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "queued", + "running", + "succeeded", + "failed", + "retried" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "in": "query", + "name": "source_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" + } + }, + { + "in": "query", + "name": "article_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Article Id" + } + }, + { + "in": "query", + "name": "topic_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + { + "in": "query", + "name": "retryable_only", + "required": false, + "schema": { + "default": false, + "title": "Retryable Only", + "type": "boolean" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_AdminJobOut_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Jobs", + "tags": [ + "admin-jobs" + ] + } + }, + "/v1/admin/jobs/{kind}/{job_id}": { + "get": { + "operationId": "admin_job_get", + "parameters": [ + { + "in": "path", + "name": "kind", + "required": true, + "schema": { + "enum": [ + "ingestion", + "source-discovery", + "partner-evaluation", + "partner-pipeline", + "article-enrichment", + "images", + "source-enrichment", + "analysis", + "topic-analysis", + "notifications" + ], + "title": "Kind", + "type": "string" + } + }, + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminJobOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Detail", + "tags": [ + "admin-jobs" + ] + } + }, + "/v1/admin/jobs/{kind}/{job_id}/logs": { + "get": { + "operationId": "admin_job_logs", + "parameters": [ + { + "in": "path", + "name": "kind", + "required": true, + "schema": { + "enum": [ + "ingestion", + "source-discovery", + "partner-evaluation", + "partner-pipeline", + "article-enrichment", + "images", + "source-enrichment", + "analysis", + "topic-analysis", + "notifications" + ], + "title": "Kind", + "type": "string" + } + }, + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + }, + { + "in": "query", + "name": "after", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 41, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "After" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 500, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminJobLogs" + } + } + }, + "description": "Successful Response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Job not found" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Log storage unavailable" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Runtime Logs", + "tags": [ + "admin-jobs" + ] + } + }, + "/v1/admin/jobs/{kind}/{job_id}/retry": { + "post": { + "operationId": "admin_job_retry", + "parameters": [ + { + "in": "path", + "name": "kind", + "required": true, + "schema": { + "enum": [ + "ingestion", + "source-discovery", + "partner-evaluation", + "partner-pipeline", + "article-enrichment", + "images", + "source-enrichment", + "analysis", + "topic-analysis", + "notifications" + ], + "title": "Kind", + "type": "string" + } + }, + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminJobOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Retry", + "tags": [ + "admin-jobs" + ] + } + }, + "/v1/admin/notifications/config": { + "get": { + "operationId": "admin_notification_config", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationConfig" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Config", + "tags": [ + "admin-notifications" + ] + } + }, + "/v1/admin/notifications/deliveries/{identifier}/retry": { + "post": { + "operationId": "admin_notification_retry", + "parameters": [ + { + "in": "path", + "name": "identifier", + "required": true, + "schema": { + "format": "uuid", + "title": "Identifier", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminJobOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Retry", + "tags": [ + "admin-notifications" + ] + } + }, + "/v1/admin/overview": { + "get": { + "operationId": "admin_overview", + "parameters": [ + { + "in": "query", + "name": "days", + "required": false, + "schema": { + "default": 30, + "maximum": 90, + "minimum": 1, + "title": "Days", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOverview" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Overview", + "tags": [ + "admin-overview" + ] + } + }, + "/v1/admin/overview/panels/{panel}": { + "get": { + "operationId": "admin_overview_panel", + "parameters": [ + { + "in": "path", + "name": "panel", + "required": true, + "schema": { + "enum": [ + "publications", + "clicks", + "accounts", + "publication-time", + "attention", + "readers", + "click-depth", + "new-accounts", + "adoption", + "publishing", + "reader-activity", + "popular-articles", + "topic-coverage", + "feed-health", + "recommendation-reasons", + "sources", + "publication-automation", + "job-tokens", + "job-reliability", + "workload", + "blockers", + "throughput", + "decision-efficiency", + "inference-tokens", + "tokens-by-task", + "tokens-by-model", + "reasoning-effort", + "inference-outcomes", + "web-searches", + "topic-outcomes", + "topic-backlog", + "repeated-stages", + "inference-duration" + ], + "title": "Panel", + "type": "string" + } + }, + { + "in": "query", + "name": "days", + "required": false, + "schema": { + "default": 30, + "maximum": 90, + "minimum": 1, + "title": "Days", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OverviewPanel" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Overview Panel", + "tags": [ + "admin-overview" + ] + } + }, + "/v1/admin/partner-accounts": { + "get": { + "operationId": "accounts_v1_admin_partner_accounts_get", + "parameters": [ + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 200, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountsOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Accounts", + "tags": [ + "admin-partner-accounts" + ] + }, + "post": { + "operationId": "create_account_v1_admin_partner_accounts_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountInput" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Create Account", + "tags": [ + "admin-partner-accounts" + ] + } + }, + "/v1/admin/partner-accounts/audience-topics": { + "get": { + "operationId": "adminPartnerAudienceTopicsCatalog", + "parameters": [ + { + "in": "query", + "name": "dimension", + "required": true, + "schema": { + "enum": [ + "languages", + "tools" + ], + "title": "Dimension", + "type": "string" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "topic_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Topic Id" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudienceTopicsOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Audience Catalog", + "tags": [ + "admin-partner-accounts" + ] + } + }, + "/v1/admin/partner-accounts/sponsor-articles": { + "get": { + "operationId": "adminPartnerSponsorArticlesCatalog", + "parameters": [ + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "article_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Article Id" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SponsorArticlesOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Sponsor Article Catalog", + "tags": [ + "admin-partner-accounts" + ] + } + }, + "/v1/admin/partner-accounts/tiers": { + "get": { + "operationId": "partnership_tiers_v1_admin_partner_accounts_tiers_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PartnershipTierOut" + }, + "title": "Response Partnership Tiers V1 Admin Partner Accounts Tiers Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Partnership Tiers", + "tags": [ + "admin-partner-accounts" + ] + } + }, + "/v1/admin/partner-accounts/{account_id}": { + "put": { + "operationId": "update_account_v1_admin_partner_accounts__account_id__put", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountInput" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } }, - "title": "Queues", - "type": "array" + "description": "Invalid or unauthorized request" }, - "workers": { - "items": { - "$ref": "#/components/schemas/WorkerOut" + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } }, - "title": "Workers", - "type": "array" + "description": "Service or dependency unavailable" } }, - "required": [ - "generated_at", - "workers", - "queues", - "ai_cooldown_seconds" + "security": [ + { + "APIKeyCookie": [] + } ], - "title": "WorkersSnapshot", - "type": "object" + "summary": "Update Account", + "tags": [ + "admin-partner-accounts" + ] } }, - "securitySchemes": { - "APIKeyCookie": { - "description": "Admin session cookie (devfeed_admin_session in HTTP development).", - "in": "cookie", - "name": "__Host-devfeed_admin_session", - "type": "apiKey" - } - } - }, - "info": { - "description": "Private administration API. OIDC sessions and CSRF protection required.", - "title": "DevFeed Admin API", - "version": "0.0.52" - }, - "openapi": "3.1.0", - "paths": { - "/health/live": { - "get": { - "operationId": "live_health_live_get", + "/v1/admin/partner-accounts/{account_id}/assets": { + "post": { + "operationId": "create_asset_v1_admin_partner_accounts__account_id__assets_post", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetInput" + } + } + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HealthResponse" + "$ref": "#/components/schemas/AssetOut" } } }, @@ -9888,21 +18525,58 @@ "description": "Service or dependency unavailable" } }, - "summary": "Live", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Create Asset", "tags": [ - "health" + "admin-partner-accounts" ] } }, - "/health/ready": { - "get": { - "operationId": "ready_health_ready_get", + "/v1/admin/partner-accounts/{account_id}/assets/{asset_id}": { + "put": { + "operationId": "update_asset_v1_admin_partner_accounts__account_id__assets__asset_id__put", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "path", + "name": "asset_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Asset Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetInput" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HealthResponse" + "$ref": "#/components/schemas/AssetOut" } } }, @@ -9928,42 +18602,195 @@ }, "description": "Invalid or unauthorized request" }, - "503": { + "5XX": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnhealthyResponse" + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Update Asset", + "tags": [ + "admin-partner-accounts" + ] + } + }, + "/v1/admin/partner-accounts/{account_id}/assets/{asset_id}/metrics": { + "put": { + "operationId": "record_metrics_v1_admin_partner_accounts__account_id__assets__asset_id__metrics_put", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "path", + "name": "asset_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Asset Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetricInput" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Record Metrics", + "tags": [ + "admin-partner-accounts" + ] + } + }, + "/v1/admin/partner-accounts/{account_id}/audience-topics": { + "get": { + "operationId": "adminPartnerAudienceTopics", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "query", + "name": "dimension", + "required": true, + "schema": { + "enum": [ + "languages", + "tools" + ], + "title": "Dimension", + "type": "string" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "topic_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" } - } - }, - "description": "Not ready" + ], + "title": "Topic Id" + } }, - "5XX": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - }, - "description": "Service or dependency unavailable" + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } } - }, - "summary": "Ready", - "tags": [ - "health" - ] - } - }, - "/v1/admin/ai/connection": { - "get": { - "operationId": "admin_ai_connection", + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CodexStatus" + "$ref": "#/components/schemas/AudienceTopicsOut" } } }, @@ -10005,21 +18832,56 @@ "APIKeyCookie": [] } ], - "summary": "Status", + "summary": "Audience Topics", "tags": [ - "admin-ai-connection" + "admin-partner-accounts" ] } }, - "/v1/admin/ai/connection/login": { - "post": { - "operationId": "admin_ai_login", + "/v1/admin/partner-accounts/{account_id}/campaigns": { + "get": { + "operationId": "adminPartnerCampaignsList", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 200, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeviceLogin" + "$ref": "#/components/schemas/CampaignsOut" } } }, @@ -10061,27 +18923,44 @@ "APIKeyCookie": [] } ], - "summary": "Login", + "summary": "Campaigns", "tags": [ - "admin-ai-connection" + "admin-partner-accounts" ] - } - }, - "/v1/admin/ai/connection/login/cancel": { + }, "post": { - "operationId": "admin_ai_login_cancel", + "operationId": "adminPartnerCampaignCreate", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CancelLogin" + "$ref": "#/components/schemas/CampaignInput" } } }, "required": true }, "responses": { - "204": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignOut" + } + } + }, "description": "Successful Response" }, "422": { @@ -10120,159 +18999,34 @@ "APIKeyCookie": [] } ], - "summary": "Cancel", + "summary": "Create Campaign", "tags": [ - "admin-ai-connection" + "admin-partner-accounts" ] } }, - "/v1/admin/articles": { + "/v1/admin/partner-accounts/{account_id}/campaigns/{campaign_id}": { "get": { - "operationId": "admin_articles_list", + "operationId": "adminPartnerCampaignGet", "parameters": [ { - "in": "query", - "name": "review_status", - "required": false, - "schema": { - "anyOf": [ - { - "enum": [ - "pending", - "approved", - "rejected" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Review Status" - } - }, - { - "in": "query", - "name": "publication_status", - "required": false, - "schema": { - "anyOf": [ - { - "enum": [ - "unpublished", - "published" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Publication Status" - } - }, - { - "in": "query", - "name": "source_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Source Id" - } - }, - { - "in": "query", - "name": "topic_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Topic Id" - } - }, - { - "in": "query", - "name": "tag_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Tag Id" - } - }, - { - "in": "query", - "name": "q", - "required": false, - "schema": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", - "type": "string" - } - }, - { - "in": "query", - "name": "sort", - "required": false, - "schema": { - "anyOf": [ - { - "maxLength": 50, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Sort" - } - }, - { - "in": "query", - "name": "limit", - "required": false, + "in": "path", + "name": "account_id", + "required": true, "schema": { - "default": 25, - "maximum": 100, - "minimum": 1, - "title": "Limit", - "type": "integer" + "format": "uuid", + "title": "Account Id", + "type": "string" } }, { - "in": "query", - "name": "offset", - "required": false, + "in": "path", + "name": "campaign_id", + "required": true, "schema": { - "default": 0, - "maximum": 9223372036854775807, - "minimum": 0, - "title": "Offset", - "type": "integer" + "format": "uuid", + "title": "Campaign Id", + "type": "string" } } ], @@ -10281,7 +19035,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_AdminArticleOut_" + "$ref": "#/components/schemas/CampaignOut" } } }, @@ -10323,29 +19077,51 @@ "APIKeyCookie": [] } ], - "summary": "Articles", + "summary": "Campaign", "tags": [ - "admin-articles" + "admin-partner-accounts" ] }, - "post": { - "operationId": "admin_article_create", + "put": { + "operationId": "adminPartnerCampaignUpdate", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "path", + "name": "campaign_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Campaign Id", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminArticleCreate" + "$ref": "#/components/schemas/CampaignInput" } } }, "required": true }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminArticleOut" + "$ref": "#/components/schemas/CampaignOut" } } }, @@ -10387,29 +19163,53 @@ "APIKeyCookie": [] } ], - "summary": "Create", + "summary": "Update Campaign", "tags": [ - "admin-articles" + "admin-partner-accounts" ] } }, - "/v1/admin/articles/{article_id}": { - "delete": { - "operationId": "admin_article_delete", + "/v1/admin/partner-accounts/{account_id}/creative-preview": { + "get": { + "operationId": "adminPartnerCreativePreview", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", + "title": "Account Id", "type": "string" } + }, + { + "in": "query", + "name": "product_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Id" + } } ], "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreativePreviewOut" + } + } + }, "description": "Successful Response" }, "422": { @@ -10448,21 +19248,148 @@ "APIKeyCookie": [] } ], - "summary": "Remove", + "summary": "Preview Creative", "tags": [ - "admin-articles" + "admin-partner-accounts" ] - }, + } + }, + "/v1/admin/partner-accounts/{account_id}/dashboard": { "get": { - "operationId": "admin_article_get", + "operationId": "dashboard_v1_admin_partner_accounts__account_id__dashboard_get", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "query", + "name": "days", + "required": false, + "schema": { + "default": 30, + "maximum": 365, + "minimum": 1, + "title": "Days", + "type": "integer" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 200, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "query", + "name": "ad_format", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "feed", + "homepage_sponsorship", + "sponsored_content", + "engagement" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Ad Format" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Dashboard" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Dashboard", + "tags": [ + "admin-partner-accounts" + ] + } + }, + "/v1/admin/partner-accounts/{account_id}/destination-preview": { + "get": { + "operationId": "adminPartnerDestinationPreview", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", "type": "string" } } @@ -10472,7 +19399,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminArticleOut" + "$ref": "#/components/schemas/DestinationPreviewOut" } } }, @@ -10514,41 +19441,95 @@ "APIKeyCookie": [] } ], - "summary": "Detail", + "summary": "Preview Destination", "tags": [ - "admin-articles" + "admin-partner-accounts" ] - }, - "put": { - "operationId": "admin_article_update", + } + }, + "/v1/admin/partner-accounts/{account_id}/inventory": { + "get": { + "operationId": "adminPartnerInventoryList", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { - "format": "uuid", - "title": "Article Id", - "type": "string" + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 200, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "product_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Id" + } + }, + { + "in": "query", + "name": "include_archived", + "required": false, + "schema": { + "default": false, + "title": "Include Archived", + "type": "boolean" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminArticleUpdate" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminArticleOut" + "$ref": "#/components/schemas/InventoryOut" } } }, @@ -10590,43 +19571,37 @@ "APIKeyCookie": [] } ], - "summary": "Update", + "summary": "Inventory", "tags": [ - "admin-articles" + "admin-partner-accounts" ] } }, - "/v1/admin/articles/{article_id}/classify": { - "post": { - "operationId": "admin_article_classify", + "/v1/admin/partner-accounts/{account_id}/members": { + "get": { + "operationId": "members_v1_admin_partner_accounts__account_id__members_get", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", + "title": "Account Id", "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClassifyArticle" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminArticleOut" + "items": { + "$ref": "#/components/schemas/MemberOut" + }, + "title": "Response Members V1 Admin Partner Accounts Account Id Members Get", + "type": "array" } } }, @@ -10668,44 +19643,37 @@ "APIKeyCookie": [] } ], - "summary": "Classify", + "summary": "Members", "tags": [ - "admin-articles" + "admin-partner-accounts" ] - } - }, - "/v1/admin/articles/{article_id}/content": { - "get": { - "operationId": "admin_article_content", + }, + "put": { + "operationId": "add_member_v1_admin_partner_accounts__account_id__members_put", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", + "title": "Account Id", "type": "string" } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ArticleContentOut" - }, - { - "type": "null" - } - ], - "title": "Response Admin Article Content" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemberInput" } - }, + } + }, + "required": true + }, + "responses": { + "204": { "description": "Successful Response" }, "422": { @@ -10744,46 +19712,41 @@ "APIKeyCookie": [] } ], - "summary": "Content", + "summary": "Add Member", "tags": [ - "admin-articles" + "admin-partner-accounts" ] } }, - "/v1/admin/articles/{article_id}/review": { - "post": { - "operationId": "admin_article_review", + "/v1/admin/partner-accounts/{account_id}/members/{subject}": { + "delete": { + "operationId": "remove_member_v1_admin_partner_accounts__account_id__members__subject__delete", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "path", + "name": "subject", + "required": true, + "schema": { + "maxLength": 200, + "minLength": 1, + "pattern": "^[A-Za-z0-9_-]+$", + "title": "Subject", "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReviewArticle" - } - } - }, - "required": true - }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminArticleOut" - } - } - }, + "204": { "description": "Successful Response" }, "422": { @@ -10822,86 +19785,43 @@ "APIKeyCookie": [] } ], - "summary": "Review", + "summary": "Remove Member", "tags": [ - "admin-articles" + "admin-partner-accounts" ] } }, - "/v1/admin/articles/{article_id}/reviews": { - "get": { - "operationId": "admin_article_reviews", + "/v1/admin/partner-accounts/{account_id}/products": { + "post": { + "operationId": "adminPartnerProductCreate", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", - "type": "string" - } - }, - { - "in": "query", - "name": "q", - "required": false, - "schema": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", + "title": "Account Id", "type": "string" } - }, - { - "in": "query", - "name": "sort", - "required": false, - "schema": { - "anyOf": [ - { - "maxLength": 50, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Sort" - } - }, - { - "in": "query", - "name": "limit", - "required": false, - "schema": { - "default": 25, - "maximum": 100, - "minimum": 1, - "title": "Limit", - "type": "integer" - } - }, - { - "in": "query", - "name": "offset", - "required": false, - "schema": { - "default": 0, - "maximum": 9223372036854775807, - "minimum": 0, - "title": "Offset", - "type": "integer" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManualProductInput" + } + } + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_ArticleReviewOut_" + "$ref": "#/components/schemas/ProductDetailOut" } } }, @@ -10943,83 +19863,40 @@ "APIKeyCookie": [] } ], - "summary": "Reviews", + "summary": "Create Product", "tags": [ - "admin-articles" + "admin-partner-accounts" ] - } - }, - "/v1/admin/auth/callback": { - "get": { - "operationId": "admin_auth_callback", - "parameters": [ - { - "in": "query", - "name": "state", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "State" - } - }, - { - "in": "query", - "name": "code", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Code" - } - }, - { - "in": "query", - "name": "error", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error" - } - }, - { - "in": "query", - "name": "iss", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Iss" + } + }, + "/v1/admin/partner-accounts/{account_id}/products/promotions": { + "get": { + "operationId": "adminPartnerProductPromotionsList", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" } } ], "responses": { - "302": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ProductPromotionOut" + }, + "title": "Response Adminpartnerproductpromotionslist", + "type": "array" + } + } + }, "description": "Successful Response" }, "422": { @@ -11053,24 +19930,44 @@ "description": "Service or dependency unavailable" } }, - "summary": "Callback", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Product Promotions", "tags": [ - "admin-auth" + "admin-partner-accounts" ] } }, - "/v1/admin/auth/config": { - "get": { - "operationId": "admin_auth_config", + "/v1/admin/partner-accounts/{account_id}/products/{product_id}": { + "delete": { + "operationId": "adminPartnerProductArchive", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "path", + "name": "product_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Product Id", + "type": "string" + } + } + ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthConfig" - } - } - }, + "204": { "description": "Successful Response" }, "422": { @@ -11104,49 +20001,49 @@ "description": "Service or dependency unavailable" } }, - "summary": "Config", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Archive Product", "tags": [ - "admin-auth" + "admin-partner-accounts" ] - } - }, - "/v1/admin/auth/login": { + }, "get": { - "operationId": "admin_auth_login", + "operationId": "adminPartnerProductGet", "parameters": [ { - "in": "query", - "name": "reauthenticate", - "required": false, + "in": "path", + "name": "account_id", + "required": true, "schema": { - "default": false, - "title": "Reauthenticate", - "type": "boolean" + "format": "uuid", + "title": "Account Id", + "type": "string" } }, { - "in": "query", - "name": "provider", - "required": false, + "in": "path", + "name": "product_id", + "required": true, "schema": { - "anyOf": [ - { - "enum": [ - "github", - "google" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Provider" + "format": "uuid", + "title": "Product Id", + "type": "string" } } ], "responses": { - "302": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductDetailOut" + } + } + }, "description": "Successful Response" }, "422": { @@ -11180,17 +20077,59 @@ "description": "Service or dependency unavailable" } }, - "summary": "Login", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Get Product", "tags": [ - "admin-auth" + "admin-partner-accounts" ] - } - }, - "/v1/admin/auth/logout": { - "post": { - "operationId": "admin_auth_logout", + }, + "patch": { + "operationId": "adminPartnerProductUpdate", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "path", + "name": "product_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Product Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManualProductUpdate" + } + } + }, + "required": true + }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductDetailOut" + } + } + }, "description": "Successful Response" }, "422": { @@ -11229,21 +20168,53 @@ "APIKeyCookie": [] } ], - "summary": "Logout", + "summary": "Update Product", "tags": [ - "admin-auth" + "admin-partner-accounts" ] } }, - "/v1/admin/auth/me": { - "get": { - "operationId": "admin_auth_me", + "/v1/admin/partner-accounts/{account_id}/products/{product_id}/ad-image": { + "post": { + "operationId": "adminPartnerProductAdImageUpload", + "parameters": [ + { + "in": "path", + "name": "account_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Account Id", + "type": "string" + } + }, + { + "in": "path", + "name": "product_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Product Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_adminPartnerProductAdImageUpload" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminIdentity" + "$ref": "#/components/schemas/ProductPromotionOut" } } }, @@ -11285,78 +20256,35 @@ "APIKeyCookie": [] } ], - "summary": "Me", + "summary": "Upload Ad Image", "tags": [ - "admin-auth" + "admin-partner-accounts" ] } }, - "/v1/admin/automation/articles/{article_id}/decisions": { + "/v1/admin/partner-accounts/{account_id}/products/{product_id}/promotion": { "get": { - "operationId": "admin_publication_decisions", + "operationId": "adminPartnerProductPromotionGet", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", + "title": "Account Id", "type": "string" } }, { - "in": "query", - "name": "q", - "required": false, + "in": "path", + "name": "product_id", + "required": true, "schema": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", + "format": "uuid", + "title": "Product Id", "type": "string" } - }, - { - "in": "query", - "name": "sort", - "required": false, - "schema": { - "anyOf": [ - { - "maxLength": 50, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Sort" - } - }, - { - "in": "query", - "name": "limit", - "required": false, - "schema": { - "default": 25, - "maximum": 100, - "minimum": 1, - "title": "Limit", - "type": "integer" - } - }, - { - "in": "query", - "name": "offset", - "required": false, - "schema": { - "default": 0, - "maximum": 9223372036854775807, - "minimum": 0, - "title": "Offset", - "type": "integer" - } } ], "responses": { @@ -11364,7 +20292,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_PublicationDecisionOut_" + "$ref": "#/components/schemas/ProductPromotionOut" } } }, @@ -11406,37 +20334,31 @@ "APIKeyCookie": [] } ], - "summary": "Publication Decisions", + "summary": "Get Product Promotion", "tags": [ - "admin-automation" + "admin-partner-accounts" ] - } - }, - "/v1/admin/automation/articles/{article_id}/{action}": { - "post": { - "operationId": "admin_automation_recover", + }, + "put": { + "operationId": "adminPartnerProductPromotionUpdate", "parameters": [ { "in": "path", - "name": "article_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", + "title": "Account Id", "type": "string" } }, { "in": "path", - "name": "action", + "name": "product_id", "required": true, "schema": { - "enum": [ - "enrich", - "analyze", - "evaluate" - ], - "title": "Action", + "format": "uuid", + "title": "Product Id", "type": "string" } } @@ -11445,7 +20367,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RecoveryRequest" + "$ref": "#/components/schemas/ProductPromotionInput" } } }, @@ -11456,7 +20378,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RecoveryResult" + "$ref": "#/components/schemas/ProductPromotionOut" } } }, @@ -11498,23 +20420,23 @@ "APIKeyCookie": [] } ], - "summary": "Recover", + "summary": "Update Product Promotion", "tags": [ - "admin-automation" + "admin-partner-accounts" ] } }, - "/v1/admin/automation/sources/{source_id}/policies": { + "/v1/admin/partner-accounts/{account_id}/sponsor-articles": { "get": { - "operationId": "admin_publication_policy_history", + "operationId": "adminPartnerSponsorArticles", "parameters": [ { "in": "path", - "name": "source_id", + "name": "account_id", "required": true, "schema": { "format": "uuid", - "title": "Source Id", + "title": "Account Id", "type": "string" } }, @@ -11532,42 +20454,41 @@ }, { "in": "query", - "name": "sort", + "name": "article_id", "required": false, "schema": { "anyOf": [ { - "maxLength": 50, + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Sort" + "title": "Article Id" } }, { "in": "query", - "name": "limit", + "name": "offset", "required": false, "schema": { - "default": 25, - "maximum": 100, - "minimum": 1, - "title": "Limit", + "default": 0, + "minimum": 0, + "title": "Offset", "type": "integer" } }, { "in": "query", - "name": "offset", + "name": "limit", "required": false, "schema": { - "default": 0, - "maximum": 9223372036854775807, - "minimum": 0, - "title": "Offset", + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", "type": "integer" } } @@ -11577,7 +20498,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_PublicationPolicyReviewOut_" + "$ref": "#/components/schemas/SponsorArticlesOut" } } }, @@ -11619,19 +20540,35 @@ "APIKeyCookie": [] } ], - "summary": "Publication Policy History", + "summary": "Account Sponsor Articles", "tags": [ - "admin-automation" + "admin-partner-accounts" ] } }, - "/v1/admin/ingestion/article-jobs": { + "/v1/admin/partner-tools": { "get": { - "operationId": "article_jobs_v1_admin_ingestion_article_jobs_get", + "operationId": "admin_partner_tools_list", "parameters": [ { "in": "query", - "name": "article_id", + "name": "provider", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" + } + }, + { + "in": "query", + "name": "product_id", "required": false, "schema": { "anyOf": [ @@ -11643,7 +20580,7 @@ "type": "null" } ], - "title": "Article Id" + "title": "Product Id" } }, { @@ -11654,10 +20591,11 @@ "anyOf": [ { "enum": [ - "queued", - "running", - "succeeded", - "failed" + "pending", + "approved", + "rejected", + "paused", + "withdrawn" ], "type": "string" }, @@ -11668,12 +20606,41 @@ "title": "Status" } }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, { "in": "query", "name": "limit", "required": false, "schema": { - "default": 50, + "default": 25, "maximum": 100, "minimum": 1, "title": "Limit", @@ -11686,6 +20653,7 @@ "required": false, "schema": { "default": 0, + "maximum": 9223372036854775807, "minimum": 0, "title": "Offset", "type": "integer" @@ -11697,11 +20665,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/ArticleEnrichmentJobOut" - }, - "title": "Response Article Jobs V1 Admin Ingestion Article Jobs Get", - "type": "array" + "$ref": "#/components/schemas/Page_ProductOut_" } } }, @@ -11743,25 +20707,68 @@ "APIKeyCookie": [] } ], - "summary": "Article Jobs", + "summary": "Listing", "tags": [ - "admin-ingestion" + "admin-partner-tools" ] } }, - "/v1/admin/ingestion/article-jobs/{job_id}": { + "/v1/admin/partner-tools/connections": { "get": { - "operationId": "article_job_detail_v1_admin_ingestion_article_jobs__job_id__get", + "operationId": "admin_partner_connections_list", "parameters": [ { - "in": "path", - "name": "job_id", - "required": true, + "in": "query", + "name": "q", + "required": false, "schema": { - "format": "uuid", - "title": "Job Id", + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", "type": "string" } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 50, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } } ], "responses": { @@ -11769,7 +20776,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ArticleEnrichmentJobOut" + "$ref": "#/components/schemas/Page_ConnectionOut_" } } }, @@ -11811,76 +20818,87 @@ "APIKeyCookie": [] } ], - "summary": "Article Job Detail", + "summary": "Connections", "tags": [ - "admin-ingestion" + "admin-partner-tools" ] - } - }, - "/v1/admin/ingestion/jobs": { - "get": { - "operationId": "jobs_v1_admin_ingestion_jobs_get", - "parameters": [ - { - "in": "query", - "name": "source_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Source Id" + }, + "post": { + "operationId": "admin_partner_connection_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectionCreate" + } } }, - { - "in": "query", - "name": "status", - "required": false, - "schema": { - "anyOf": [ - { - "enum": [ - "queued", - "running", - "succeeded", - "failed" - ], - "type": "string" - }, - { - "type": "null" + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectionOut" } - ], - "title": "Status" - } + } + }, + "description": "Successful Response" }, - { - "in": "query", - "name": "limit", - "required": false, - "schema": { - "default": 50, - "maximum": 100, - "minimum": 1, - "title": "Limit", - "type": "integer" - } + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ { - "in": "query", - "name": "offset", - "required": false, + "APIKeyCookie": [] + } + ], + "summary": "Create Connection", + "tags": [ + "admin-partner-tools" + ] + } + }, + "/v1/admin/partner-tools/connections/{provider}": { + "get": { + "operationId": "admin_partner_connection_get", + "parameters": [ + { + "in": "path", + "name": "provider", + "required": true, "schema": { - "default": 0, - "minimum": 0, - "title": "Offset", - "type": "integer" + "title": "Provider", + "type": "string" } } ], @@ -11889,11 +20907,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/JobOut" - }, - "title": "Response Jobs V1 Admin Ingestion Jobs Get", - "type": "array" + "$ref": "#/components/schemas/ConnectionOut" } } }, @@ -11935,33 +20949,40 @@ "APIKeyCookie": [] } ], - "summary": "Jobs", + "summary": "Connection", "tags": [ - "admin-ingestion" + "admin-partner-tools" ] - } - }, - "/v1/admin/ingestion/jobs/{job_id}": { - "get": { - "operationId": "job_detail_v1_admin_ingestion_jobs__job_id__get", + }, + "post": { + "operationId": "admin_partner_connection_action", "parameters": [ { "in": "path", - "name": "job_id", + "name": "provider", "required": true, "schema": { - "format": "uuid", - "title": "Job Id", + "title": "Provider", "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectionAction" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/JobOut" + "$ref": "#/components/schemas/ConnectionOut" } } }, @@ -12003,21 +21024,40 @@ "APIKeyCookie": [] } ], - "summary": "Job Detail", + "summary": "Connection Action", "tags": [ - "admin-ingestion" + "admin-partner-tools" ] - } - }, - "/v1/admin/ingestion/status": { - "get": { - "operationId": "status_v1_admin_ingestion_status_get", + }, + "put": { + "operationId": "admin_partner_connection_update", + "parameters": [ + { + "in": "path", + "name": "provider", + "required": true, + "schema": { + "title": "Provider", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectionSettings" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IngestionStatus" + "$ref": "#/components/schemas/ConnectionOut" } } }, @@ -12059,181 +21099,217 @@ "APIKeyCookie": [] } ], - "summary": "Status", + "summary": "Update Connection", "tags": [ - "admin-ingestion" + "admin-partner-tools" ] } }, - "/v1/admin/jobs/ai-analysis": { + "/v1/admin/partner-tools/connections/{provider}/jobs": { "get": { - "description": "Page article and topic research runs together before loading their details.", - "operationId": "admin_ai_analysis_jobs_list", + "operationId": "admin_partner_connection_jobs", "parameters": [ { - "in": "query", - "name": "status", - "required": false, + "in": "path", + "name": "provider", + "required": true, "schema": { - "anyOf": [ - { - "enum": [ - "queued", - "running", - "succeeded", - "failed", - "retried" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" + "title": "Provider", + "type": "string" } }, { "in": "query", - "name": "analysis_type", + "name": "q", "required": false, "schema": { - "anyOf": [ - { - "enum": [ - "articles", - "topics" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Analysis Type" + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", + "type": "string" } }, { "in": "query", - "name": "article_id", + "name": "sort", "required": false, "schema": { "anyOf": [ { - "format": "uuid", + "maxLength": 50, "type": "string" }, { "type": "null" } ], - "title": "Article Id" + "title": "Sort" } }, { "in": "query", - "name": "proposal_id", + "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Proposal Id" + "default": 25, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" } }, { "in": "query", - "name": "topic_id", + "name": "offset", "required": false, "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Topic Id" + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_PartnerJobOut_" + } + } + }, + "description": "Successful Response" }, - { - "in": "query", - "name": "retryable_only", - "required": false, - "schema": { - "default": false, - "title": "Retryable Only", - "type": "boolean" - } + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ { - "in": "query", - "name": "q", - "required": false, - "schema": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", - "type": "string" + "APIKeyCookie": [] + } + ], + "summary": "Connection Jobs", + "tags": [ + "admin-partner-tools" + ] + } + }, + "/v1/admin/partner-tools/connector-preview": { + "post": { + "operationId": "admin_partner_connector_preview", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectorPreview" + } } }, - { - "in": "query", - "name": "sort", - "required": false, - "schema": { - "anyOf": [ - { - "maxLength": 50, - "type": "string" - }, - { - "type": "null" + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectorPreviewOut" } - ], - "title": "Sort" - } + } + }, + "description": "Successful Response" }, - { - "in": "query", - "name": "limit", - "required": false, - "schema": { - "default": 25, - "maximum": 100, - "minimum": 1, - "title": "Limit", - "type": "integer" - } + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ { - "in": "query", - "name": "offset", - "required": false, - "schema": { - "default": 0, - "maximum": 9223372036854775807, - "minimum": 0, - "title": "Offset", - "type": "integer" - } + "APIKeyCookie": [] } ], + "summary": "Preview Connector", + "tags": [ + "admin-partner-tools" + ] + } + }, + "/v1/admin/partner-tools/connector-response": { + "post": { + "operationId": "admin_partner_connector_response", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectorPreview" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_AdminJobOut_" + "$ref": "#/components/schemas/ConnectorResponseOut" } } }, @@ -12275,35 +21351,16 @@ "APIKeyCookie": [] } ], - "summary": "Ai Analysis Jobs", + "summary": "Connector Response", "tags": [ - "admin-jobs" + "admin-partner-tools" ] } }, - "/v1/admin/jobs/{kind}": { + "/v1/admin/partner-tools/evaluations": { "get": { - "operationId": "admin_jobs_list", + "operationId": "admin_partner_evaluations_list", "parameters": [ - { - "in": "path", - "name": "kind", - "required": true, - "schema": { - "enum": [ - "ingestion", - "source-discovery", - "article-enrichment", - "images", - "source-enrichment", - "analysis", - "topic-analysis", - "notifications" - ], - "title": "Kind", - "type": "string" - } - }, { "in": "query", "name": "status", @@ -12315,8 +21372,7 @@ "queued", "running", "succeeded", - "failed", - "retried" + "failed" ], "type": "string" }, @@ -12329,24 +21385,7 @@ }, { "in": "query", - "name": "source_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Source Id" - } - }, - { - "in": "query", - "name": "article_id", + "name": "product_id", "required": false, "schema": { "anyOf": [ @@ -12358,34 +21397,23 @@ "type": "null" } ], - "title": "Article Id" + "title": "Product Id" } }, { "in": "query", - "name": "topic_id", + "name": "provider", "required": false, "schema": { "anyOf": [ { - "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Topic Id" - } - }, - { - "in": "query", - "name": "retryable_only", - "required": false, - "schema": { - "default": false, - "title": "Retryable Only", - "type": "boolean" + "title": "Provider" } }, { @@ -12447,7 +21475,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_AdminJobOut_" + "$ref": "#/components/schemas/Page_PartnerEvaluationSummary_" } } }, @@ -12489,35 +21517,16 @@ "APIKeyCookie": [] } ], - "summary": "Jobs", + "summary": "Evaluations", "tags": [ - "admin-jobs" + "admin-partner-tools" ] } }, - "/v1/admin/jobs/{kind}/{job_id}": { + "/v1/admin/partner-tools/evaluations/{job_id}": { "get": { - "operationId": "admin_job_get", + "operationId": "admin_partner_evaluation_get", "parameters": [ - { - "in": "path", - "name": "kind", - "required": true, - "schema": { - "enum": [ - "ingestion", - "source-discovery", - "article-enrichment", - "images", - "source-enrichment", - "analysis", - "topic-analysis", - "notifications" - ], - "title": "Kind", - "type": "string" - } - }, { "in": "path", "name": "job_id", @@ -12534,7 +21543,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminJobOut" + "$ref": "#/components/schemas/PartnerEvaluationDetail" } } }, @@ -12576,60 +21585,136 @@ "APIKeyCookie": [] } ], - "summary": "Detail", + "summary": "Evaluation Detail", "tags": [ - "admin-jobs" + "admin-partner-tools" ] } }, - "/v1/admin/jobs/{kind}/{job_id}/logs": { + "/v1/admin/partner-tools/pipeline": { "get": { - "operationId": "admin_job_logs", + "operationId": "admin_partner_pipeline_list", "parameters": [ { - "in": "path", - "name": "kind", - "required": true, + "in": "query", + "name": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "queued", + "running", + "succeeded", + "failed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "in": "query", + "name": "operation", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "sync", + "sync_product", + "assess" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Operation" + } + }, + { + "in": "query", + "name": "provider", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" + } + }, + { + "in": "query", + "name": "product_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Product Id" + } + }, + { + "in": "query", + "name": "parent_id", + "required": false, "schema": { - "enum": [ - "ingestion", - "source-discovery", - "article-enrichment", - "images", - "source-enrichment", - "analysis", - "topic-analysis", - "notifications" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } ], - "title": "Kind", - "type": "string" + "title": "Parent Id" } }, { - "in": "path", - "name": "job_id", - "required": true, + "in": "query", + "name": "q", + "required": false, "schema": { - "format": "uuid", - "title": "Job Id", + "default": "", + "maxLength": 200, + "pattern": "^[^\\x00]*$", + "title": "Q", "type": "string" } }, { "in": "query", - "name": "after", + "name": "sort", "required": false, "schema": { "anyOf": [ { - "maxLength": 41, + "maxLength": 50, "type": "string" }, { "type": "null" } ], - "title": "After" + "title": "Sort" } }, { @@ -12637,12 +21722,24 @@ "name": "limit", "required": false, "schema": { - "default": 100, - "maximum": 500, + "default": 25, + "maximum": 100, "minimum": 1, "title": "Limit", "type": "integer" } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } } ], "responses": { @@ -12650,22 +21747,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminJobLogs" + "$ref": "#/components/schemas/Page_PartnerPipelineOut_" } } }, "description": "Successful Response" }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - }, - "description": "Job not found" - }, "422": { "content": { "application/json": { @@ -12686,16 +21773,6 @@ }, "description": "Invalid or unauthorized request" }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - }, - "description": "Log storage unavailable" - }, "5XX": { "content": { "application/json": { @@ -12712,35 +21789,16 @@ "APIKeyCookie": [] } ], - "summary": "Runtime Logs", + "summary": "Pipeline", "tags": [ - "admin-jobs" + "admin-partner-tools" ] } }, - "/v1/admin/jobs/{kind}/{job_id}/retry": { - "post": { - "operationId": "admin_job_retry", + "/v1/admin/partner-tools/pipeline/{job_id}": { + "get": { + "operationId": "admin_partner_pipeline_get", "parameters": [ - { - "in": "path", - "name": "kind", - "required": true, - "schema": { - "enum": [ - "ingestion", - "source-discovery", - "article-enrichment", - "images", - "source-enrichment", - "analysis", - "topic-analysis", - "notifications" - ], - "title": "Kind", - "type": "string" - } - }, { "in": "path", "name": "job_id", @@ -12757,7 +21815,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminJobOut" + "$ref": "#/components/schemas/PartnerPipelineOut" } } }, @@ -12799,21 +21857,25 @@ "APIKeyCookie": [] } ], - "summary": "Retry", + "summary": "Pipeline Detail", "tags": [ - "admin-jobs" + "admin-partner-tools" ] } }, - "/v1/admin/notifications/config": { + "/v1/admin/partner-tools/providers": { "get": { - "operationId": "admin_notification_config", + "operationId": "admin_partner_providers_list", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationConfig" + "items": { + "$ref": "#/components/schemas/PartnerProviderOut" + }, + "title": "Response Admin Partner Providers List", + "type": "array" } } }, @@ -12855,103 +21917,43 @@ "APIKeyCookie": [] } ], - "summary": "Config", + "summary": "Providers", "tags": [ - "admin-notifications" + "admin-partner-tools" ] } }, - "/v1/admin/notifications/deliveries/{identifier}/retry": { + "/v1/admin/partner-tools/{product_id}/actions": { "post": { - "operationId": "admin_notification_retry", + "operationId": "admin_partner_product_action", "parameters": [ { "in": "path", - "name": "identifier", + "name": "product_id", "required": true, "schema": { "format": "uuid", - "title": "Identifier", + "title": "Product Id", "type": "string" } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminJobOut" - } - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - }, - "description": "Request validation failed" - }, - "4XX": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductAction" } - }, - "description": "Invalid or unauthorized request" + } }, - "5XX": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - }, - "description": "Service or dependency unavailable" - } + "required": true }, - "security": [ - { - "APIKeyCookie": [] - } - ], - "summary": "Retry", - "tags": [ - "admin-notifications" - ] - } - }, - "/v1/admin/overview": { - "get": { - "operationId": "admin_overview", - "parameters": [ - { - "in": "query", - "name": "days", - "required": false, - "schema": { - "default": 30, - "maximum": 90, - "minimum": 1, - "title": "Days", - "type": "integer" - } - } - ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminOverview" + "$ref": "#/components/schemas/ProductOut" } } }, @@ -12993,71 +21995,25 @@ "APIKeyCookie": [] } ], - "summary": "Overview", + "summary": "Product Action", "tags": [ - "admin-overview" + "admin-partner-tools" ] } }, - "/v1/admin/overview/panels/{panel}": { + "/v1/admin/partner-tools/{product_id}/evaluations": { "get": { - "operationId": "admin_overview_panel", + "operationId": "admin_partner_tools_evaluations", "parameters": [ { "in": "path", - "name": "panel", + "name": "product_id", "required": true, "schema": { - "enum": [ - "publications", - "clicks", - "accounts", - "publication-time", - "attention", - "readers", - "click-depth", - "new-accounts", - "adoption", - "publishing", - "reader-activity", - "popular-articles", - "topic-coverage", - "feed-health", - "recommendation-reasons", - "sources", - "publication-automation", - "job-tokens", - "job-reliability", - "workload", - "blockers", - "throughput", - "decision-efficiency", - "inference-tokens", - "tokens-by-task", - "tokens-by-model", - "reasoning-effort", - "inference-outcomes", - "web-searches", - "topic-outcomes", - "topic-backlog", - "repeated-stages", - "inference-duration" - ], - "title": "Panel", + "format": "uuid", + "title": "Product Id", "type": "string" } - }, - { - "in": "query", - "name": "days", - "required": false, - "schema": { - "default": 30, - "maximum": 90, - "minimum": 1, - "title": "Days", - "type": "integer" - } } ], "responses": { @@ -13065,7 +22021,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OverviewPanel" + "items": { + "$ref": "#/components/schemas/EvaluationOut" + }, + "title": "Response Admin Partner Tools Evaluations", + "type": "array" } } }, @@ -13107,9 +22067,9 @@ "APIKeyCookie": [] } ], - "summary": "Overview Panel", + "summary": "Evaluations", "tags": [ - "admin-overview" + "admin-partner-tools" ] } }, @@ -18341,6 +27301,16 @@ "get": { "operationId": "admin_users_list", "parameters": [ + { + "in": "query", + "name": "identity_only", + "required": false, + "schema": { + "default": false, + "title": "Identity Only", + "type": "boolean" + } + }, { "in": "query", "name": "interests", diff --git a/apps/admin/orval.config.ts b/apps/admin/orval.config.ts index a7ef629c8..70cd664e5 100644 --- a/apps/admin/orval.config.ts +++ b/apps/admin/orval.config.ts @@ -8,6 +8,8 @@ export default defineConfig({ tags: [ "admin-auth", "admin-users", + "admin-partner-tools", + "admin-partner-accounts", "admin-settings", "admin-ai-connection", "admin-overview", diff --git a/apps/admin/package.json b/apps/admin/package.json index 1464a94e4..4cc45c371 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -12,6 +12,7 @@ "analyze": "next analyze" }, "dependencies": { + "@devfeed/http": "0.0.1", "@chimely/client": "0.2.2", "@chimely/react": "0.2.2", "@devfeed/telemetry": "0.0.1", diff --git a/apps/admin/src/app/(admin)/partner-tools/page.tsx b/apps/admin/src/app/(admin)/partner-tools/page.tsx new file mode 100644 index 000000000..3d27b96f8 --- /dev/null +++ b/apps/admin/src/app/(admin)/partner-tools/page.tsx @@ -0,0 +1,4 @@ +import { permanentRedirect } from "next/navigation"; +export default function PartnerToolsPage() { + permanentRedirect("/partnerships/connections"); +} diff --git a/apps/admin/src/app/(admin)/partnerships/accounts/[id]/[[...section]]/page.tsx b/apps/admin/src/app/(admin)/partnerships/accounts/[id]/[[...section]]/page.tsx new file mode 100644 index 000000000..917949d8a --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/accounts/[id]/[[...section]]/page.tsx @@ -0,0 +1,47 @@ +import { notFound, redirect } from "next/navigation"; +import { requireAdmin } from "@/lib/server/session"; +import { + PartnerAccountPage, + type AccountSection, +} from "@/components/organisms/partner-account-page"; +export const metadata = { title: "Partner account" }; +export default async function Page({ + params, + searchParams, +}: { + params: Promise<{ id: string; section?: string[] }>; + searchParams: Promise<{ offset?: string; limit?: string }>; +}) { + const admin = await requireAdmin(); + if (!admin.roles.includes("superuser")) redirect("/"); + const { id, section: segments } = await params; + const section = segments?.join("/") ?? "details"; + if ( + ![ + "details", + "edit", + "related", + "products", + "campaigns", + "campaigns/new", + "members/new", + "assets/new", + ].includes(section) && + !/^(?:assets|campaigns)\/[^/]+\/edit$/.test(section) + ) + notFound(); + const query = await searchParams; + const offset = Number(query.offset ?? 0); + const limit = Number(query.limit ?? 100); + if (!Number.isSafeInteger(offset) || offset < 0) notFound(); + if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) notFound(); + return ( + + ); +} diff --git a/apps/admin/src/app/(admin)/partnerships/accounts/new/page.tsx b/apps/admin/src/app/(admin)/partnerships/accounts/new/page.tsx new file mode 100644 index 000000000..5af56bd6b --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/accounts/new/page.tsx @@ -0,0 +1,9 @@ +import { redirect } from "next/navigation"; +import { requireAdmin } from "@/lib/server/session"; +import { PartnerAccountPage } from "@/components/organisms/partner-account-page"; +export const metadata = { title: "Create partner account" }; +export default async function Page() { + const admin = await requireAdmin(); + if (!admin.roles.includes("superuser")) redirect("/"); + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/accounts/page.tsx b/apps/admin/src/app/(admin)/partnerships/accounts/page.tsx new file mode 100644 index 000000000..4ca761375 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/accounts/page.tsx @@ -0,0 +1,9 @@ +import { redirect } from "next/navigation"; +import { requireAdmin } from "@/lib/server/session"; +import { PartnerAccounts } from "@/components/organisms/partner-accounts"; +export const metadata = { title: "Partner accounts" }; +export default async function Page() { + const admin = await requireAdmin(); + if (!admin.roles.includes("superuser")) redirect("/"); + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/connections/[provider]/edit/page.tsx b/apps/admin/src/app/(admin)/partnerships/connections/[provider]/edit/page.tsx new file mode 100644 index 000000000..bad485114 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/connections/[provider]/edit/page.tsx @@ -0,0 +1,6 @@ +import { PartnerConnectionPage } from "@/components/organisms/partner-connection-page"; +export const metadata = { title: "Edit import connection" }; +export default async function Page({ params }: { params: Promise<{ provider: string }> }) { + const { provider } = await params; + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/connections/[provider]/page.tsx b/apps/admin/src/app/(admin)/partnerships/connections/[provider]/page.tsx new file mode 100644 index 000000000..806ee510b --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/connections/[provider]/page.tsx @@ -0,0 +1,6 @@ +import { PartnerConnectionPage } from "@/components/organisms/partner-connection-page"; +export const metadata = { title: "Import connection details" }; +export default async function Page({ params }: { params: Promise<{ provider: string }> }) { + const { provider } = await params; + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/connections/[provider]/related/page.tsx b/apps/admin/src/app/(admin)/partnerships/connections/[provider]/related/page.tsx new file mode 100644 index 000000000..1412d0701 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/connections/[provider]/related/page.tsx @@ -0,0 +1,6 @@ +import { PartnerConnectionPage } from "@/components/organisms/partner-connection-page"; +export const metadata = { title: "Import connection related objects" }; +export default async function Page({ params }: { params: Promise<{ provider: string }> }) { + const { provider } = await params; + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/connections/new/page.tsx b/apps/admin/src/app/(admin)/partnerships/connections/new/page.tsx new file mode 100644 index 000000000..83eb41d7c --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/connections/new/page.tsx @@ -0,0 +1,5 @@ +import { PartnerConnectionPage } from "@/components/organisms/partner-connection-page"; +export const metadata = { title: "Create import connection" }; +export default function Page() { + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/connections/page.tsx b/apps/admin/src/app/(admin)/partnerships/connections/page.tsx new file mode 100644 index 000000000..6b6b561e5 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/connections/page.tsx @@ -0,0 +1,5 @@ +import { PartnerConnections } from "@/components/organisms/partner-connections"; +export const metadata = { title: "Import connections" }; +export default function Page() { + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/evaluations/[id]/[[...section]]/page.tsx b/apps/admin/src/app/(admin)/partnerships/evaluations/[id]/[[...section]]/page.tsx new file mode 100644 index 000000000..c041c1882 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/evaluations/[id]/[[...section]]/page.tsx @@ -0,0 +1,19 @@ +import { notFound } from "next/navigation"; +import { + PartnerRunDetail, + type PartnerRunSection, +} from "@/components/organisms/partner-run-detail"; +export const metadata = { title: "Evaluations run" }; +export default async function Page({ + params, +}: { + params: Promise<{ id: string; section?: string[] }>; +}) { + const { id, section } = await params; + const tab = section?.[0] ?? "details"; + if ((section?.length ?? 0) > 1 || !["details", "related", "results", "logs"].includes(tab)) + notFound(); + return ( + + ); +} diff --git a/apps/admin/src/app/(admin)/partnerships/evaluations/page.tsx b/apps/admin/src/app/(admin)/partnerships/evaluations/page.tsx new file mode 100644 index 000000000..1fc892f0a --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/evaluations/page.tsx @@ -0,0 +1,5 @@ +import { PartnerRuns } from "@/components/organisms/partner-runs"; +export const metadata = { title: "Evaluations" }; +export default function Page() { + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/page.tsx b/apps/admin/src/app/(admin)/partnerships/page.tsx new file mode 100644 index 000000000..86d9e2b55 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/page.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; +export const metadata = { title: "Partnerships" }; +export default function Page() { + return ( +
+ +
+ {[ + { + name: "Partner accounts", + href: "/partnerships/accounts", + description: "Manage business accounts, tiers, promotion defaults, and campaigns.", + }, + { + name: "Import connections", + href: "/partnerships/connections", + description: "Configure product APIs, import mappings, and sync schedules.", + }, + { + name: "Products", + href: "/partnerships/products", + description: "View synced products, platform listings, and automatic checks.", + }, + { + name: "Pipeline jobs", + href: "/partnerships/pipeline", + description: "Inspect discovery, product syncs, qualification, and worker logs.", + }, + { + name: "Evaluations", + href: "/partnerships/evaluations", + description: "Inspect article matching runs, decisions, and evidence.", + }, + ].map((item) => ( + +
+ {item.name} + +
+

{item.description}

+ + ))} +
+
+ ); +} diff --git a/apps/admin/src/app/(admin)/partnerships/pipeline/[id]/[[...section]]/page.tsx b/apps/admin/src/app/(admin)/partnerships/pipeline/[id]/[[...section]]/page.tsx new file mode 100644 index 000000000..bb2d32b60 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/pipeline/[id]/[[...section]]/page.tsx @@ -0,0 +1,16 @@ +import { notFound } from "next/navigation"; +import { + PartnerRunDetail, + type PartnerRunSection, +} from "@/components/organisms/partner-run-detail"; +export const metadata = { title: "Pipeline jobs run" }; +export default async function Page({ + params, +}: { + params: Promise<{ id: string; section?: string[] }>; +}) { + const { id, section } = await params; + const tab = section?.[0] ?? "details"; + if ((section?.length ?? 0) > 1 || !["details", "related", "logs"].includes(tab)) notFound(); + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/pipeline/page.tsx b/apps/admin/src/app/(admin)/partnerships/pipeline/page.tsx new file mode 100644 index 000000000..0df6d318b --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/pipeline/page.tsx @@ -0,0 +1,5 @@ +import { PartnerRuns } from "@/components/organisms/partner-runs"; +export const metadata = { title: "Pipeline jobs" }; +export default function Page() { + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/products/[id]/[[...section]]/page.tsx b/apps/admin/src/app/(admin)/partnerships/products/[id]/[[...section]]/page.tsx new file mode 100644 index 000000000..5a2bc2ceb --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/products/[id]/[[...section]]/page.tsx @@ -0,0 +1,13 @@ +import { notFound } from "next/navigation"; +import { PartnerProductPage } from "@/components/organisms/partner-product-page"; +export const metadata = { title: "Product" }; +export default async function Page({ + params, +}: { + params: Promise<{ id: string; section?: string[] }>; +}) { + const { id, section } = await params; + const tab = section?.[0] ?? "details"; + if ((section?.length ?? 0) > 1 || !["details", "related"].includes(tab)) notFound(); + return ; +} diff --git a/apps/admin/src/app/(admin)/partnerships/products/page.tsx b/apps/admin/src/app/(admin)/partnerships/products/page.tsx new file mode 100644 index 000000000..2f011c060 --- /dev/null +++ b/apps/admin/src/app/(admin)/partnerships/products/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from "next/navigation"; +import { partnerProductHref } from "@/lib/partner-runs"; +import { PartnerTools } from "@/components/organisms/partner-tools"; +export const metadata = { title: "Products" }; +export default async function Page({ + searchParams, +}: { + searchParams: Promise<{ provider?: string; product_id?: string }>; +}) { + const { provider, product_id } = await searchParams; + if (product_id) redirect(partnerProductHref(product_id)); + return ; +} diff --git a/apps/admin/src/app/globals.css b/apps/admin/src/app/globals.css index b71bded67..c872db56c 100644 --- a/apps/admin/src/app/globals.css +++ b/apps/admin/src/app/globals.css @@ -1,6 +1,8 @@ @import "@devfeed/theme/retry-button.css"; @import "@devfeed/theme/theme-toggle.css"; @import "@devfeed/ui/content.css"; +@import "@devfeed/ui/choice-controls.css"; +@import "@devfeed/ui/sidebar.css"; @import "tailwindcss"; @import "tw-animate-css"; @import "@devfeed/theme/tokens.css"; @@ -8,6 +10,8 @@ @import "@devfeed/theme/notifications.css"; @import "@devfeed/theme/tailwind.css"; +@source "../../../../packages/ui/src"; + @layer base { * { @apply border-border outline-ring/50; diff --git a/apps/admin/src/components/atoms/url-input.tsx b/apps/admin/src/components/atoms/url-input.tsx index dc8935d93..12ae5e6dc 100644 --- a/apps/admin/src/components/atoms/url-input.tsx +++ b/apps/admin/src/components/atoms/url-input.tsx @@ -1,7 +1,7 @@ import type { ComponentProps } from "react"; import { LoaderCircle } from "lucide-react"; import { cn } from "@/lib/utils"; -import { Input } from "./input"; +import { Input } from "@devfeed/ui/input"; export type UrlInputProps = Omit, "type"> & { loading?: boolean; diff --git a/apps/admin/src/components/molecules/column-value.tsx b/apps/admin/src/components/molecules/column-value.tsx index c34a2b193..0dd9da70c 100644 --- a/apps/admin/src/components/molecules/column-value.tsx +++ b/apps/admin/src/components/molecules/column-value.tsx @@ -1,6 +1,6 @@ "use client"; -import { Badge } from "@/components/atoms/badge"; +import { Badge } from "@devfeed/ui/badge"; import { BooleanIndicator } from "@/components/atoms/boolean-indicator"; import { DateTime } from "@/components/molecules/date-time"; import { StatusBadge } from "@/components/molecules/status-badge"; diff --git a/apps/admin/src/components/molecules/combobox.tsx b/apps/admin/src/components/molecules/combobox.tsx index 6759fa964..eac8f71a2 100644 --- a/apps/admin/src/components/molecules/combobox.tsx +++ b/apps/admin/src/components/molecules/combobox.tsx @@ -1,20 +1 @@ -"use client"; - -import { Select, type SelectOption, type SelectProps } from "./select"; - -export type ComboboxOption = SelectOption; -export type ComboboxProps = Omit & { - search?: string; - onSearchChange?: (value: string) => void; - searchPlaceholder?: string; -}; - -/** Searchable Select. Entity and language pickers add their own data sources. */ -export function Combobox({ search, onSearchChange, searchPlaceholder, ...props }: ComboboxProps) { - return ( - + onChange( + promotionSurfaces.filter((item) => + item === surface ? event.target.checked : value.includes(item), + ), + ) + } + /> + {labels[surface]} + + ))} + + + ); +} diff --git a/apps/admin/src/components/molecules/record-actions.tsx b/apps/admin/src/components/molecules/record-actions.tsx index bf6ab141e..684acd073 100644 --- a/apps/admin/src/components/molecules/record-actions.tsx +++ b/apps/admin/src/components/molecules/record-actions.tsx @@ -1,7 +1,7 @@ import { recordHref } from "@/lib/routes"; import Link from "next/link"; import { ClipboardCheck, Pencil, RefreshCw, Tags, Trash2 } from "lucide-react"; -import { Button } from "@/components/atoms/button"; +import { Button } from "@devfeed/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/atoms/tooltip"; import { cn } from "@/lib/utils"; import { type Resource, resources } from "@/lib/resources"; diff --git a/apps/admin/src/components/molecules/relationship-add-menu.tsx b/apps/admin/src/components/molecules/relationship-add-menu.tsx index e7aac3f4e..0bfbfd814 100644 --- a/apps/admin/src/components/molecules/relationship-add-menu.tsx +++ b/apps/admin/src/components/molecules/relationship-add-menu.tsx @@ -2,13 +2,13 @@ import Link from "next/link"; import { ChevronDown, ClipboardCheck, Plus, Sparkles } from "lucide-react"; -import { Button } from "@/components/atoms/button"; +import { Button } from "@devfeed/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/atoms/dropdown-menu"; +} from "@devfeed/ui/dropdown-menu"; export function RelationshipAddMenu() { const base = "/taxonomy/relationships"; diff --git a/apps/admin/src/components/molecules/relationship-proposal-actions.tsx b/apps/admin/src/components/molecules/relationship-proposal-actions.tsx index f58b49c05..639006f4a 100644 --- a/apps/admin/src/components/molecules/relationship-proposal-actions.tsx +++ b/apps/admin/src/components/molecules/relationship-proposal-actions.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useState } from "react"; import { Check, Eye, X } from "lucide-react"; -import { Button } from "@/components/atoms/button"; +import { Button } from "@devfeed/ui/button"; import { useAdmin } from "./admin-session"; import { adminRelationshipProposalReview } from "@/lib/api/generated/admin"; import type { RelationshipProposalOut } from "@/lib/api/generated/models"; diff --git a/apps/admin/src/components/molecules/search-field.tsx b/apps/admin/src/components/molecules/search-field.tsx index 78c5f2ea5..bce71f6e2 100644 --- a/apps/admin/src/components/molecules/search-field.tsx +++ b/apps/admin/src/components/molecules/search-field.tsx @@ -2,8 +2,8 @@ import { useEffect, useRef, useState } from "react"; import { Search, X } from "lucide-react"; -import { Input } from "@/components/atoms/input"; -import { Button } from "@/components/atoms/button"; +import { Input } from "@devfeed/ui/input"; +import { Button } from "@devfeed/ui/button"; import { cn } from "@/lib/utils"; /** Query acknowledgements and paging do not reset an in-progress search. */ diff --git a/apps/admin/src/components/molecules/settings-form.tsx b/apps/admin/src/components/molecules/settings-form.tsx index e04f16031..0d0bf8238 100644 --- a/apps/admin/src/components/molecules/settings-form.tsx +++ b/apps/admin/src/components/molecules/settings-form.tsx @@ -1,8 +1,8 @@ "use client"; import { useState, type ReactNode } from "react"; -import { Button } from "@/components/atoms/button"; -import { Input } from "@/components/atoms/input"; +import { Button } from "@devfeed/ui/button"; +import { Input } from "@devfeed/ui/input"; import { Field } from "./field"; import { ValidationErrors } from "./validation-errors"; import { notify, notifyFailure } from "@/lib/notifications"; diff --git a/apps/admin/src/components/molecules/settings-nav.tsx b/apps/admin/src/components/molecules/settings-nav.tsx index fef38ac91..0a2fdc141 100644 --- a/apps/admin/src/components/molecules/settings-nav.tsx +++ b/apps/admin/src/components/molecules/settings-nav.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { Bell, Monitor, SlidersHorizontal, UserRound } from "lucide-react"; import { settingsSections } from "@/lib/settings"; -import { PageHeading } from "./page-heading"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; import { cn } from "@/lib/utils"; const icons = { profile: UserRound, @@ -16,7 +16,7 @@ export function SettingsNav() { const current = settingsSections.find((item) => item.id === active); return (
- +
+ )} + + {feedOnly ? ( +

+ {sponsoredContent + ? "Sponsored content appears in reader feeds." + : "Homepage sponsorship appears on the main homepage only."} +

+ ) : ( + + )} + {feedOnly && !account.promotion_surfaces?.includes("feed") && ( +

+ Reader feeds are disabled in this account’s defaults. Enable Reader feeds before this + format can appear. +

+ )} + +
+ {account.advanced_targeting ? ( + + {(control) => ( + { + setProvider(next); + setName(providers.find((item) => item.provider === next)?.name ?? ""); + changeConnector(connectorDefaults(next)); + }} + options={[ + ...providers.map((item) => ({ value: item.provider, label: item.name })), + ...(connection && !providers.some((item) => item.provider === connection.provider) + ? [{ value: connection.provider, label: connection.name }] + : []), + ...(!connection ? [{ value: "__custom__", label: "Custom API" }] : []), + ]} + /> + )} + + + {(control) => ( + + )} + + + {(control) => ( + setName(event.target.value)} /> + )} + + {provider === "__custom__" && ( + + {(control) => ( + setCustomId(event.target.value)} + /> + )} + + )} + + {(control) => ( + setInterval(event.target.value)} + /> + )} + + + {(control) => ( + setEnabled(event.target.checked)} + className="size-4 accent-primary" + /> + )} + + + + {preview && ( +
+

+ {preview.discovered} {preview.discovered === 1 ? "product" : "products"} found + {preview.has_next_page ? "; another page is available" : ""}. +

+ {preview.products.map((product) => ( +
+

{product.name}

+

{product.product_url}

+

{product.description}

+
+ ))} + {preview.errors.map((message, index) => ( +

+ {message} +

+ ))} + {!preview.products.length && !preview.errors.length && ( +

No products matched the filters.

+ )} +
+ )} +
+ + + +
+ + + + ); +} diff --git a/apps/admin/src/components/organisms/partner-connection-page.tsx b/apps/admin/src/components/organisms/partner-connection-page.tsx new file mode 100644 index 000000000..1f8b5ad9a --- /dev/null +++ b/apps/admin/src/components/organisms/partner-connection-page.tsx @@ -0,0 +1,348 @@ +"use client"; + +import { partnerSyncInterval } from "@/lib/partner-interval"; + +import { useRefreshInterval } from "@/lib/use-refresh-interval"; + +import Link from "next/link"; +import { Pencil, RefreshCw } from "lucide-react"; +import { ResourcePanel, DataValue } from "@devfeed/ui/resource-layout"; +import { DateTime } from "@/components/molecules/date-time"; +import { StatusBadge } from "@/components/molecules/status-badge"; +import { PartnerRelated } from "./partner-related"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Button } from "@devfeed/ui/button"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; +import { RequestState } from "@/components/molecules/request-state"; +import { useAdmin } from "@/components/molecules/admin-session"; +import { PartnerConnectionForm } from "./partner-connection-form"; +import { + importConnectionHref, + importConnectionsPath, + partnershipTrail, +} from "./partner-connections"; +import { + dashboardV1AdminPartnerAccountsAccountIdDashboardGet, + adminPartnerConnectionGet, + adminPartnerProvidersList, + adminPartnerConnectionAction, +} from "@/lib/api/generated/admin"; +import type { AccountOut, ConnectionOut, PartnerProviderOut } from "@/lib/api/generated/models"; +import { ApiError } from "@/lib/api/client"; + +export function PartnerConnectionPage({ + provider, + editing = false, + section = "details", +}: { + provider?: string; + editing?: boolean; + section?: "details" | "related"; +}) { + const refreshSeconds = useRefreshInterval(); + const router = useRouter(); + const admin = useAdmin(); + const [account, setAccount] = useState(); + const [connection, setConnection] = useState(); + const [providers, setProviders] = useState([]); + const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(); + const [refresh, setRefresh] = useState(0); + useEffect(() => { + let live = true; + async function findConnection(identifier: string) { + try { + return await adminPartnerConnectionGet(identifier); + } catch (error) { + if (error instanceof ApiError && error.status === 404) return undefined; + throw error; + } + } + async function load() { + try { + const [found, supported] = await Promise.all([ + provider ? findConnection(provider) : undefined, + !provider || editing ? adminPartnerProvidersList() : [], + ]); + if (provider && !found) throw new Error("Import connection not found."); + // Presets are a small fixed catalog. Probe each identity directly so an + // existing preset outside the connections page cannot be offered again. + const existing = provider + ? [] + : await Promise.all(supported.map((preset) => findConnection(preset.provider))); + const available = provider + ? supported + : supported.filter((_preset, index) => !existing[index]); + const dashboard = + found?.account_id && !editing && section === "details" + ? await dashboardV1AdminPartnerAccountsAccountIdDashboardGet(found.account_id, { + limit: 1, + }) + : undefined; + if (live) { + setConnection(found); + setAccount(dashboard?.account); + setProviders(available); + setError(undefined); + } + } catch (error) { + if (live) + setError(error instanceof Error ? error : new Error("Could not load import connection.")); + } finally { + if (live) setLoading(false); + } + } + void load(); + // Keep the edit snapshot stable so a concurrent settings change returns a conflict. + const timer = + provider && !editing && refreshSeconds > 0 + ? setInterval(() => void load(), refreshSeconds * 1000) + : undefined; + return () => { + live = false; + clearInterval(timer); + }; + }, [provider, editing, section, refresh, refreshSeconds]); + const trail = [ + ...partnershipTrail, + { label: "Import connections", href: importConnectionsPath }, + ...(editing && connection + ? [{ label: connection.name, href: importConnectionHref(connection.provider) }] + : []), + ]; + async function sync() { + if (!connection) return; + setBusy(true); + setError(undefined); + try { + setConnection( + await adminPartnerConnectionAction( + connection.provider, + { action: "sync" }, + { headers: { "X-CSRF-Token": admin.csrf_token } }, + ), + ); + } catch (error) { + setError(error instanceof Error ? error : new Error("Could not sync import connection.")); + } finally { + setBusy(false); + } + } + return ( +
+ + {connection && !editing && ( + <> + + + + )} + + setRefresh((n) => n + 1)} /> + {!loading && !error && (!provider || editing) && ( + router.push(importConnectionHref(saved.provider))} + onCancel={() => + router.push( + connection ? importConnectionHref(connection.provider) : importConnectionsPath, + ) + } + /> + )} + {connection && !editing && ( + <> + + {section === "related" ? ( + + ) : ( +
+
+ }, + { label: "Type", value: "Launch platform" }, + { + label: "Partner account", + value: connection.account_id ? ( + + {account?.name} + + ) : ( + "Unassigned" + ), + }, + { + label: "Enabled", + value: , + }, + { label: "API", value: }, + { + label: "Sync interval", + value: partnerSyncInterval(connection.sync_interval_minutes), + }, + ]} + /> + {account && ( + +

+ Existing and newly imported products follow these account settings + automatically. Contextual matches are preferred; sponsored feed placements can + fill unmatched feeds. Article and topic placements require a relevant match. +

+ + Edit partner account defaults + +
+ )} + + {connection.products} + + ), + }, + ...[ + ["Accepted for account", connection.commercial_approved ?? 0], + ["Qualified", connection.qualified], + ["Checking", connection.checking], + ["Needs attention", connection.needs_attention], + ["Excluded", connection.excluded], + ].map(([label, value]) => ({ + label: String(label), + value: , + })), + ]} + /> +
+
+ , + }, + { label: "Revision", value: }, + ]} + /> + + ), + }, + { + label: "Last synced", + value: connection.last_sync_at ? ( + + ) : ( + "Not synced yet" + ), + }, + { + label: "Next sync", + value: connection.next_sync_at ? ( + + ) : ( + "Not scheduled" + ), + }, + { label: "Last error", value: }, + ]} + /> + {connection.enabled && !connection.ai_enabled && ( +

+ Automatic product checks are waiting for AI to be enabled in settings. +

+ )} +
+
+ )} + + )} +
+ ); +} diff --git a/apps/admin/src/components/organisms/partner-connections.tsx b/apps/admin/src/components/organisms/partner-connections.tsx new file mode 100644 index 000000000..23fe71d61 --- /dev/null +++ b/apps/admin/src/components/organisms/partner-connections.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { partnerSyncInterval } from "@/lib/partner-interval"; + +import { useRefreshInterval } from "@/lib/use-refresh-interval"; + +import Link from "next/link"; +import { useCallback, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Plus } from "lucide-react"; +import { Button } from "@devfeed/ui/button"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; +import { SearchField, searchScope } from "@/components/molecules/search-field"; +import { useRequest } from "@/lib/use-request"; +import { useTableQuery } from "@/lib/use-table-query"; +import { DataTable, type DataTableColumn } from "@/components/molecules/data-table"; +import { adminPartnerConnectionsList } from "@/lib/api/generated/admin"; +import type { ConnectionOut } from "@/lib/api/generated/models"; + +export const importConnectionsPath = "/partnerships/connections"; +export const importConnectionHref = (provider: string) => + `${importConnectionsPath}/${encodeURIComponent(provider)}`; +export const partnershipTrail = [{ label: "Partnerships", href: "/partnerships" }]; +const columns: DataTableColumn[] = [ + { + id: "name", + accessorKey: "name", + header: "Name", + enableSorting: true, + cell: ({ row }) => ( + + {row.original.name} + + ), + }, + { id: "type", header: "Type", cell: () => "Launch platform" }, + { + id: "sync_interval", + header: "Sync interval", + accessorFn: (row) => partnerSyncInterval(row.sync_interval_minutes), + }, + + { + id: "enabled", + header: "Enabled", + accessorFn: (row) => (row.enabled ? "Enabled" : "Disabled"), + kind: "pill", + }, + { + id: "state", + header: "Sync status", + accessorFn: (row) => + !row.enabled + ? "Paused" + : row.state === "syncing" + ? "Syncing" + : row.state === "error" + ? "Failed" + : "Idle", + kind: "pill", + }, + { id: "products", header: "Products", accessorKey: "products", kind: "number" }, + { + id: "commercial_approved", + header: "Accepted for account", + accessorFn: (row) => row.commercial_approved ?? 0, + kind: "number", + }, + { id: "last_sync_at", header: "Last synced", accessorKey: "last_sync_at", kind: "datetime" }, + { + id: "actions", + header: "Actions", + enableHiding: false, + cell: ({ row }) => ( + + ), + }, +]; + +export function PartnerConnections() { + const query = useTableQuery("partner-connections").toString(); + return ; +} + +function ConnectionTable({ query }: { query: string }) { + const router = useRouter(); + const params = new URLSearchParams(query); + const bounded = (value: string | null, fallback: number, max: number) => + value !== null && /^\d+$/.test(value) ? Math.min(max, Number(value)) : fallback; + const offset = bounded(params.get("offset"), 0, 100000000); + const limit = Math.max(1, bounded(params.get("limit"), 25, 100)); + const requestedSort = params.get("sort") ?? "provider"; + const sort = ["provider", "-provider", "name", "-name"].includes(requestedSort) + ? requestedSort + : "provider"; + const q = params.get("q") ?? ""; + const refreshSeconds = useRefreshInterval(); + const [refresh, setRefresh] = useState(0); + const load = useCallback( + async (signal: AbortSignal) => + adminPartnerConnectionsList({ q, sort, offset, limit }, { signal }), + [q, sort, offset, limit], + ); + const result = useRequest(`partner-connections/${query}/${refresh}`, load, refreshSeconds * 1000); + function change(values: Record) { + const next = new URLSearchParams(query); + next.set("offset", "0"); + Object.entries(values).forEach(([key, value]) => + value ? next.set(key, value) : next.delete(key), + ); + router.replace(`${importConnectionsPath}?${next}`, { scroll: false }); + } + return ( +
+ + + +

+ Connect product APIs and manage imports into partner accounts. +

+ ({ enableSorting: false, ...column }))} + getRowId={(row) => row.provider} + columnChoices + loading={result.loading} + error={result.error} + onRetry={() => setRefresh((n) => n + 1)} + sort={sort} + onSortChange={(next) => change({ sort: next })} + pagination={{ offset, limit, total: result.data?.total ?? 0, onChange: change }} + toolbar={ + change({ q: value })} + /> + } + empty={ + q + ? "No import connections match your search." + : "No import connections. Create an import connection to configure a product API." + } + /> +
+ ); +} diff --git a/apps/admin/src/components/organisms/partner-destination-preview.tsx b/apps/admin/src/components/organisms/partner-destination-preview.tsx new file mode 100644 index 000000000..a249d6989 --- /dev/null +++ b/apps/admin/src/components/organisms/partner-destination-preview.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { ProductDestinationPreview } from "@devfeed/ui/product-destination-preview"; +import { adminPartnerDestinationPreview } from "@/lib/api/generated/admin"; +import { useRequest } from "@/lib/use-request"; + +type Props = { accountId?: string; destination: "product_url" | "listing_url" }; + +function AccountPreview({ accountId, destination }: Props & { accountId: string }) { + const [revision, setRevision] = useState(0); + const load = useCallback( + (signal: AbortSignal) => adminPartnerDestinationPreview(accountId, { signal }), + [accountId], + ); + const result = useRequest(`partner-destination-preview/${accountId}/${revision}`, load); + return ( + setRevision((value) => value + 1)} + /> + ); +} + +export function PartnerDestinationPreview({ accountId, destination }: Props) { + return accountId ? ( + + ) : ( + + ); +} diff --git a/apps/admin/src/components/organisms/partner-inventory.tsx b/apps/admin/src/components/organisms/partner-inventory.tsx new file mode 100644 index 000000000..38732cc64 --- /dev/null +++ b/apps/admin/src/components/organisms/partner-inventory.tsx @@ -0,0 +1,204 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useState } from "react"; +import { Button } from "@devfeed/ui/button"; +import { DataTable, type DataTableColumn } from "@/components/molecules/data-table"; +import { RequestState } from "@/components/molecules/request-state"; +import { SearchField } from "@/components/molecules/search-field"; +import { StatusBadge } from "@/components/molecules/status-badge"; +import { useAdmin } from "@/components/molecules/admin-session"; +import { + adminPartnerInventoryList, + adminPartnerProductPromotionUpdate, +} from "@/lib/api/generated/admin"; +import type { AccountOut, InventoryProductOut } from "@/lib/api/generated/models"; +import { ApiError, returnToLogin } from "@/lib/api/client"; +import { useRequest } from "@/lib/use-request"; +import { partnerProductHref } from "@/lib/partner-runs"; +import { PartnerProductPlacements } from "./partner-ad-formats"; + +export function PartnerInventory({ account }: { account: AccountOut }) { + const admin = useAdmin(); + const [query, setQuery] = useState(""); + const [page, setPage] = useState({ offset: 0, limit: 25 }); + const [revision, setRevision] = useState(0); + const [busy, setBusy] = useState(); + const [error, setError] = useState(); + const load = useCallback( + (signal: AbortSignal) => + adminPartnerInventoryList(account.id, { ...page, q: query || undefined }, { signal }), + [account.id, page, query], + ); + const result = useRequest( + `partner-inventory/${account.id}/${query}/${page.offset}/${page.limit}/${revision}`, + load, + ); + async function toggle(product: InventoryProductOut) { + setBusy(product.id); + setError(undefined); + try { + await adminPartnerProductPromotionUpdate( + account.id, + product.id, + { excluded: !product.excluded }, + { headers: { "X-CSRF-Token": admin.csrf_token } }, + ); + setRevision((value) => value + 1); + } catch (reason) { + if (reason instanceof ApiError && reason.status === 401) returnToLogin(); + setError(reason instanceof Error ? reason : new Error("Could not update product.")); + } finally { + setBusy(undefined); + } + } + const columns: DataTableColumn[] = [ + { + accessorKey: "name", + header: "Product", + meta: { headerClassName: "w-[24%]", className: "whitespace-normal" }, + cell: ({ row }) => ( +
+ + {row.original.name} + +
+ Developer check + +
+
+ ), + }, + { + accessorKey: "commercial_approved", + header: "Acceptance", + meta: { headerClassName: "w-[18%]", className: "whitespace-normal" }, + cell: ({ row }) => ( +
+ + {row.original.commercial_approved && ( +

+ {row.original.approval_source === "trusted" ? "Trusted partner" : "Verified product"} +

+ )} +
+ ), + }, + { + accessorKey: "technologies", + header: "Technologies", + meta: { + headerClassName: "w-[28%]", + className: "whitespace-normal break-words leading-5 text-muted-foreground", + }, + cell: ({ row }) => row.original.technologies.join(", ") || "—", + }, + { + accessorKey: "excluded", + header: "Promotion", + meta: { headerClassName: "w-[16%]", className: "whitespace-normal" }, + cell: ({ row }) => ( +
+ {row.original.globally_excluded ? ( + "Blocked by product checks" + ) : row.original.excluded ? ( + + ) : row.original.ad_formats != null || row.original.promotion_surfaces != null ? ( + "Custom restrictions" + ) : ( + "Inherits account defaults" + )} +
+ ), + }, + { + id: "actions", + header: "Actions", + meta: { headerClassName: "w-[14%]", className: "whitespace-normal" }, + cell: ({ row }) => ( +
+ + setRevision((value) => value + 1)} + /> +
+ ), + }, + ]; + return ( +
+
+
+

Imported products

+

+ Products inherit this account’s promotion defaults. +

+
+ { + setQuery(value); + setPage((current) => ({ ...current, offset: 0 })); + }} + /> +
+ {!account.auto_include_products && ( +

+ Automatic inclusion is off. Products can only appear through campaigns that select them + explicitly until you enable automatic inclusion in account settings. +

+ )} + + row.id} + loading={result.loading} + error={result.error} + onRetry={() => setRevision((value) => value + 1)} + empty="No matching products. Products added directly or imported into this account appear here." + pagination={{ + ...page, + total: result.data?.total ?? 0, + onChange: (values) => + setPage((current) => ({ + offset: Number(values.offset ?? current.offset), + limit: Number(values.limit ?? current.limit), + })), + }} + /> +
+ ); +} diff --git a/apps/admin/src/components/organisms/partner-product-page.tsx b/apps/admin/src/components/organisms/partner-product-page.tsx new file mode 100644 index 000000000..a17d9b1b6 --- /dev/null +++ b/apps/admin/src/components/organisms/partner-product-page.tsx @@ -0,0 +1,396 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useState } from "react"; +import { Button } from "@devfeed/ui/button"; +import { useAdmin } from "@/components/molecules/admin-session"; +import { DateTime } from "@/components/molecules/date-time"; +import { ResourcePanel, DataValue } from "@devfeed/ui/resource-layout"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; +import { RequestState } from "@/components/molecules/request-state"; +import { StatusBadge } from "@/components/molecules/status-badge"; +import { adminPartnerToolsList, adminPartnerProductAction } from "@/lib/api/generated/admin"; +import { partnerProductHref } from "@/lib/partner-runs"; +import { useRefreshInterval } from "@/lib/use-refresh-interval"; +import { useRequest } from "@/lib/use-request"; +import { importConnectionHref, partnershipTrail } from "./partner-connections"; +import { RelatedPartnerRuns } from "./partner-runs"; + +export function PartnerProductPage({ + id, + section = "details", +}: { + id: string; + section?: "details" | "related"; +}) { + const admin = useAdmin(); + const interval = useRefreshInterval(); + const [revision, setRevision] = useState(0); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(); + const load = useCallback( + async (signal: AbortSignal) => { + const page = await adminPartnerToolsList({ product_id: id, limit: 1 }, { signal }); + if (!page.items[0]) throw new Error("Product not found."); + return page.items[0]; + }, + [id], + ); + const result = useRequest(`partner-product/${id}/${revision}`, load, interval * 1000); + const product = result.data; + async function act(action: "exclude" | "retry") { + if (!product) return; + setBusy(true); + setError(undefined); + try { + await adminPartnerProductAction( + product.id, + { action, expected_revision: product.revision }, + { headers: { "X-CSRF-Token": admin.csrf_token } }, + ); + setRevision((n) => n + 1); + } catch (error) { + setError(error instanceof Error ? error : new Error("Could not update product.")); + } finally { + setBusy(false); + } + } + return ( +
+ + {product && ( + <> + + {!product.excluded && ( + + )} + + )} + + setRevision((n) => n + 1)} + /> + {product && ( + <> + + {section === "related" ? ( +
+ {!!product.manual_sources?.length && ( +
+

+ Direct accounts ({product.manual_sources.length}) +

+
+ {product.manual_sources.map((source) => ( + + {source.account_name} + + ), + }, + { label: "Source", value: "Added directly" }, + { + label: "Status", + value: , + }, + { + label: "Account acceptance", + value: ( + + ), + }, + { label: "Updated", value: }, + ]} + /> + ))} +
+
+ )} + {!!product.listings.length && ( +
+

Platform listings ({product.listings.length})

+
+ {product.listings.map((listing) => ( + + {listing.platform_name} + + ), + }, + { label: "Name", value: listing.name }, + { + label: "Partner account", + value: listing.account_id ? ( + + {listing.account_name ?? "View partner account"} + + ) : ( + "Unassigned" + ), + }, + { + label: "Account acceptance", + value: ( + + ), + }, + { + label: "Platform product ID", + value: , + }, + { + label: "Status", + value: ( + + ), + }, + { + label: "Identity", + value: , + }, + { + label: "Identity details", + value: , + }, + { + label: "Description", + value: , + }, + { label: "Pricing", value: }, + { + label: "Attribution", + value: , + }, + { + label: "Display information source", + value: , + }, + { label: "Website", value: }, + { + label: "Listing", + value: ( + + View {listing.platform_name} listing + + ), + }, + { label: "Updated", value: }, + ]} + /> + ))} +
+
+ )} + + +
+ ) : ( +
+
+ }, + { label: "Description", value: }, + { label: "Pricing", value: }, + { label: "Technologies", value: }, + ]} + /> + + {product.evidence?.length ? ( +
+ {product.evidence.map((evidence, index) => ( +
+

{evidence.quote}

+ +
+ ))} +
+ ) : ( +

No verified evidence yet.

+ )} +
+ +
+

+ {product.commercial_approved + ? "Accepted for associated partner accounts" + : "Awaiting account acceptance"} + {product.approval_sources?.includes("trusted") && + " · trusted account products"} +

+

+ This product inherits promotion settings from each account that added it + directly or imported it through a connection. Account and tier defaults + control product or sponsored appearances; trusted account products can be + accepted automatically while capability checks continue. Contextual matches + are preferred; sponsored feed placements can fill unmatched feeds. Article and + topic placements require a relevant match. Exclusions still apply. +

+ + Manage partner account defaults and campaigns + +
+
+
+
+ + ), + }, + { label: "Reason", value: }, + { + label: "Eligible for matching", + value: , + }, + { + label: "Verified", + value: product.verified_at ? ( + + ) : ( + "Not verified yet" + ), + }, + ]} + /> + }, + { label: "Revision", value: }, + { label: "Updated", value: }, + { + label: "Sources", + value: ( + + {[ + ...new Set(product.listings.map((listing) => listing.platform_name)), + ...(product.manual_sources ?? []).map( + (source) => `Direct · ${source.account_name}`, + ), + ].join(", ")} + + ), + }, + ]} + /> +
+
+ )} + + )} +
+ ); +} diff --git a/apps/admin/src/components/organisms/partner-related.tsx b/apps/admin/src/components/organisms/partner-related.tsx new file mode 100644 index 000000000..275a8e4cd --- /dev/null +++ b/apps/admin/src/components/organisms/partner-related.tsx @@ -0,0 +1,75 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useState } from "react"; +import { DataTable } from "@/components/molecules/data-table"; +import { useRequest } from "@/lib/use-request"; +import { useRefreshInterval } from "@/lib/use-refresh-interval"; +import { adminPartnerToolsList } from "@/lib/api/generated/admin"; +import { productColumns } from "./partner-tools"; +import { RelatedPartnerRuns } from "./partner-runs"; + +export function PartnerRelated({ provider }: { provider: string }) { + return ( +
+ + + +
+ ); +} + +function RelatedProducts({ provider }: { provider: string }) { + const refreshSeconds = useRefreshInterval(); + const [page, setPage] = useState({ offset: 0, limit: 25 }); + const [sort, setSort] = useState("-updated_at"); + const [revision, setRevision] = useState(0); + const load = useCallback( + (signal: AbortSignal) => adminPartnerToolsList({ provider, ...page, sort }, { signal }), + [provider, page, sort], + ); + const result = useRequest( + `partner-products/${provider}/${JSON.stringify(page)}/${sort}/${revision}`, + load, + refreshSeconds * 1000, + ); + return ( +
+
+

Products{result.data ? ` (${result.data.total})` : ""}

+ + View full list + +
+ ({ enableSorting: false, ...column }))} + sort={sort} + onSortChange={(next) => { + setSort(next); + setPage((current) => ({ ...current, offset: 0 })); + }} + getRowId={(row) => row.id} + columnChoices + loading={result.loading} + error={result.error} + onRetry={() => setRevision((n) => n + 1)} + pagination={{ + ...page, + total: result.data?.total ?? 0, + onChange: (values) => + setPage((current) => ({ + offset: Number(values.offset ?? current.offset), + limit: Number(values.limit ?? current.limit), + })), + }} + empty="No products synced from this import connection yet." + /> +
+ ); +} diff --git a/apps/admin/src/components/organisms/partner-run-detail.tsx b/apps/admin/src/components/organisms/partner-run-detail.tsx new file mode 100644 index 000000000..d1f4c7ef1 --- /dev/null +++ b/apps/admin/src/components/organisms/partner-run-detail.tsx @@ -0,0 +1,342 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useState } from "react"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; +import { ResourcePanel, DataValue } from "@devfeed/ui/resource-layout"; +import { DateTime } from "@/components/molecules/date-time"; +import { StatusBadge } from "@/components/molecules/status-badge"; +import { RequestState } from "@/components/molecules/request-state"; +import { DataTable } from "@/components/molecules/data-table"; +import { useRequest } from "@/lib/use-request"; +import { useRefreshInterval } from "@/lib/use-refresh-interval"; +import { adminPartnerPipelineGet, adminPartnerEvaluationGet } from "@/lib/api/generated/admin"; +import type { PartnerPipelineOut, PartnerEvaluationDetail } from "@/lib/api/generated/models"; +import { + partnerRunLabels, + partnerRunHref, + partnerRunPath, + partnerProductHref, + partnerOperation, + type PartnerRunKind, +} from "@/lib/partner-runs"; +import { importConnectionHref, partnershipTrail } from "./partner-connections"; +import { RelatedPartnerRuns } from "./partner-runs"; +import { JobLogs } from "./job-logs"; + +export type PartnerRunSection = "details" | "related" | "results" | "logs"; +const linked = (href: string, label: string) => ( + + {label} + +); + +export function PartnerRunDetail({ + kind, + id, + section = "details", +}: { + kind: PartnerRunKind; + id: string; + section?: PartnerRunSection; +}) { + const interval = useRefreshInterval(); + const [revision, setRevision] = useState(0); + const load = useCallback( + async (signal: AbortSignal): Promise => + kind === "pipeline" + ? adminPartnerPipelineGet(id, { signal }) + : adminPartnerEvaluationGet(id, { signal }), + [kind, id], + ); + const result = useRequest(`partner-${kind}/${id}/${revision}`, load, interval * 1000); + const run = result.data; + const evaluation = run && "snapshot" in run ? run : undefined; + const pipeline = run && "operation" in run ? run : undefined; + const tabs: PartnerRunSection[] = [ + "details", + "related", + ...(kind === "evaluations" ? ["results" as const] : []), + "logs", + ]; + return ( +
+ + + setRevision((n) => n + 1)} + /> + {run && section === "details" && ( +
+
+ + ), + }, + { + label: "Platform product ID", + value: , + }, + { + label: "Parent run", + value: pipeline.parent_id + ? linked( + partnerRunHref("pipeline", pipeline.parent_id), + pipeline.parent_id, + ) + : "—", + }, + ] + : evaluation + ? [ + { + label: "Product", + value: linked( + partnerProductHref(evaluation.product_id), + evaluation.snapshot.product.name, + ), + }, + { label: "Product revision", value: evaluation.snapshot.product.revision }, + { label: "Articles evaluated", value: evaluation.snapshot.articles.length }, + { + label: "Matches", + value: evaluation.result?.decisions.filter((d) => d.relevant).length ?? 0, + }, + { + label: "Results", + value: + evaluation.status === "succeeded" + ? evaluation.current + ? "Current" + : "Outdated — product or article data changed" + : "Not completed", + }, + ] + : [] + } + /> + }, + { label: "Attempts", value: run.attempts }, + { label: "Available at", value: }, + { label: "Error", value: }, + ]} + /> +
+ }, + { label: "Created", value: }, + { + label: "Finished", + value: run.finished_at ? : "—", + }, + ]} + /> +
+ )} + {run && section === "related" && ( +
+ + {pipeline?.operation === "sync" && ( + + )} + {pipeline?.product_id && ( + + )} + {evaluation && ( + article.id} + columns={[ + { + id: "title", + header: "Article", + accessorKey: "title", + cell: ({ row }) => + linked(`/content/articles/${row.original.id}`, row.original.title), + }, + { id: "type", header: "Type", accessorKey: "content_type" }, + { + id: "match", + header: "Result", + accessorFn: (article) => { + const decision = evaluation.result?.decisions.find( + (d) => d.article_id === article.id, + ); + return decision + ? decision.relevant + ? "Relevant" + : "No match" + : "Not evaluated"; + }, + }, + ]} + empty="No articles in this evaluation." + /> + )} +
+ )} + {evaluation && section === "results" && } + {run && (section === "logs" || section === "details") && ( + + )} +
+ ); +} + +function EvaluationResults({ run }: { run: PartnerEvaluationDetail }) { + return ( +
+ {run.status === "succeeded" && !run.current && ( +

+ These results are outdated because product or article data changed. +

+ )} + {!run.result?.decisions.length && ( +

+ {run.status === "failed" + ? "Evaluation failed. See Details and Logs for the error." + : run.status === "succeeded" + ? "No article decisions were recorded." + : "Evaluation results will appear when this run completes."} +

+ )} + {run.result?.decisions.map((decision) => { + const article = run.snapshot.articles.find((a) => a.id === decision.article_id); + const evidence = run.snapshot.product.evidence?.[decision.evidence_index]; + return ( + + ), + }, + { label: "Reason", value: }, + { label: "Technology", value: }, + { label: "Article evidence", value: }, + ...(evidence + ? [ + { + label: "Product evidence", + value: ( + <> +

{evidence.quote}

+ + + ), + }, + ] + : []), + ]} + /> + ); + })} +
+ ); +} diff --git a/apps/admin/src/components/organisms/partner-runs.tsx b/apps/admin/src/components/organisms/partner-runs.tsx new file mode 100644 index 000000000..fc31c163c --- /dev/null +++ b/apps/admin/src/components/organisms/partner-runs.tsx @@ -0,0 +1,294 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; +import { DataTable, type DataTableColumn } from "@/components/molecules/data-table"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; +import { SearchField, searchScope } from "@/components/molecules/search-field"; +import { Select } from "@/components/molecules/select"; +import { Button } from "@devfeed/ui/button"; +import { useRequest } from "@/lib/use-request"; +import { useTableQuery } from "@/lib/use-table-query"; +import { useRefreshInterval } from "@/lib/use-refresh-interval"; +import { adminPartnerPipelineList, adminPartnerEvaluationsList } from "@/lib/api/generated/admin"; +import { + partnerRunLabels, + partnerRunHref, + partnerRunPath, + partnerProductHref, + partnerOperation, + type PartnerRunKind, +} from "@/lib/partner-runs"; +import { importConnectionHref, partnershipTrail } from "./partner-connections"; + +type Run = { + id: string; + status: string; + product_id: string | null; + product_name: string | null; + created_at: string; + finished_at: string | null; + attempts: number; + error: string | null; + provider?: string | null; + operation?: string; + external_id?: string | null; + matches?: number; + articles?: number; +}; +type RunPage = { items: Run[]; total: number; offset: number; limit: number }; + +export function PartnerRuns({ kind }: { kind: PartnerRunKind }) { + const query = useTableQuery(`partner-${kind}`).toString(); + const router = useRouter(); + return ( +
+ + router.replace(`${partnerRunPath(kind)}?${next}`, { scroll: false })} + /> +
+ ); +} + +export function RelatedPartnerRuns({ + kind, + filters, + title, +}: { + kind: PartnerRunKind; + filters: Record; + title?: string; +}) { + const [query, setQuery] = useState(""); + const params = new URLSearchParams(query); + Object.entries(filters).forEach(([key, value]) => params.set(key, value)); + return ( +
+
+

{title ?? partnerRunLabels[kind]}

+ + View full list + +
+ +
+ ); +} + +function RunTable({ + kind, + query, + onQuery, + embedded = false, +}: { + kind: PartnerRunKind; + query: string; + onQuery: (next: string) => void; + embedded?: boolean; +}) { + const params = new URLSearchParams(query); + const bounded = (value: string | null, fallback: number, max: number) => + value !== null && /^\d+$/.test(value) ? Math.min(max, Number(value)) : fallback; + const offset = bounded(params.get("offset"), 0, 100000000); + const limit = Math.max(1, bounded(params.get("limit"), 25, 100)); + const sort = params.get("sort") ?? "-created_at"; + const status = params.get("status") ?? ""; + const operation = params.get("operation") ?? ""; + const q = params.get("q") ?? ""; + const provider = params.get("provider") || undefined; + const productId = params.get("product_id") || undefined; + const parentId = params.get("parent_id") || undefined; + const [revision, setRevision] = useState(0); + const interval = useRefreshInterval(); + function change(values: Record) { + const next = new URLSearchParams(query); + next.set("offset", "0"); + Object.entries(values).forEach(([key, value]) => + value ? next.set(key, value) : next.delete(key), + ); + onQuery(next.toString()); + } + const load = useCallback( + async (signal: AbortSignal): Promise => { + const validStatus = ["queued", "running", "succeeded", "failed"].includes(status) + ? (status as "queued" | "running" | "succeeded" | "failed") + : undefined; + const common = { + q, + status: validStatus, + sort, + offset, + limit, + provider, + product_id: productId, + }; + return kind === "pipeline" + ? adminPartnerPipelineList( + { + ...common, + parent_id: parentId, + operation: ["sync", "sync_product", "assess"].includes(operation) + ? (operation as "sync" | "sync_product" | "assess") + : undefined, + }, + { signal }, + ) + : adminPartnerEvaluationsList(common, { signal }); + }, + [kind, q, status, sort, offset, limit, provider, productId, parentId, operation], + ); + const result = useRequest(`partner-${kind}/${query}/${revision}`, load, interval * 1000); + const columns: DataTableColumn[] = [ + { + id: "id", + header: "Run", + accessorKey: "id", + cell: ({ row }) => ( + + {row.original.id.slice(0, 8)} + + ), + }, + ...(kind === "pipeline" + ? [ + { + id: "operation", + header: "Operation", + accessorFn: (row: Run) => partnerOperation(row.operation ?? ""), + }, + { + id: "provider", + header: "Import connection", + cell: ({ row }: { row: { original: Run } }) => + row.original.provider ? ( + + {row.original.provider} + + ) : ( + "Direct product" + ), + }, + ] + : []), + { + id: "product", + header: "Product", + cell: ({ row }) => + row.original.product_id ? ( + + {row.original.product_name ?? + row.original.external_id ?? + row.original.product_id.slice(0, 8)} + + ) : ( + (row.original.external_id ?? "—") + ), + }, + { id: "status", header: "Status", accessorKey: "status", kind: "pill", enableSorting: true }, + { + id: "attempts", + header: "Attempts", + accessorKey: "attempts", + kind: "number", + enableSorting: true, + }, + ...(kind === "evaluations" + ? [{ id: "matches", header: "Matches", accessorKey: "matches", kind: "number" as const }] + : []), + { + id: "created_at", + header: "Created", + accessorKey: "created_at", + kind: "datetime", + enableSorting: true, + }, + { id: "error", header: "Error", accessorKey: "error" }, + ]; + return ( + ({ enableSorting: false, ...column }))} + getRowId={(row) => row.id} + columnChoices + loading={result.loading} + error={result.error} + onRetry={() => setRevision((n) => n + 1)} + sort={sort} + onSortChange={(next) => change({ sort: next })} + pagination={{ offset, limit, total: result.data?.total ?? 0, onChange: change }} + empty={`No ${partnerRunLabels[kind].toLowerCase()} found.`} + toolbar={ + !embedded && ( + <> + change({ q })} + /> + change({ operation: value === "all" ? "" : value })} + options={[ + { value: "all", label: "All operations" }, + ...["sync", "sync_product", "assess"].map((value) => ({ + value, + label: partnerOperation(value), + })), + ]} + /> + )} + + {(provider || productId || parentId) && ( + + )} + + ) + } + /> + ); +} diff --git a/apps/admin/src/components/organisms/partner-tools.tsx b/apps/admin/src/components/organisms/partner-tools.tsx new file mode 100644 index 000000000..80b5f7895 --- /dev/null +++ b/apps/admin/src/components/organisms/partner-tools.tsx @@ -0,0 +1,145 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; +import { Button } from "@devfeed/ui/button"; +import { DataTable, type DataTableColumn } from "@/components/molecules/data-table"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; +import { SearchField, searchScope } from "@/components/molecules/search-field"; +import { adminPartnerToolsList } from "@/lib/api/generated/admin"; +import type { ProductOut } from "@/lib/api/generated/models"; +import { partnerProductHref } from "@/lib/partner-runs"; +import { useRequest } from "@/lib/use-request"; +import { useTableQuery } from "@/lib/use-table-query"; +import { useRefreshInterval } from "@/lib/use-refresh-interval"; +import { partnershipTrail } from "./partner-connections"; + +export const productColumns: DataTableColumn[] = [ + { + id: "name", + header: "Name", + accessorKey: "name", + enableSorting: true, + cell: ({ row }) => ( + + {row.original.name} + + ), + }, + { + id: "assessment", + header: "Assessment", + accessorFn: (row) => (row.excluded ? "Excluded" : row.assessment.state), + kind: "pill", + }, + { + id: "account_acceptance", + header: "Account acceptance", + accessorFn: (row) => + row.commercial_approved + ? row.approval_sources?.includes("trusted") + ? "Accepted · trusted partner" + : "Accepted · verified" + : "Awaiting acceptance", + kind: "pill", + }, + { + id: "platforms", + header: "Sources", + accessorFn: (row) => + [ + ...new Set(row.listings.map((listing) => listing.platform_name)), + ...(row.manual_sources ?? []).map((source) => `Direct · ${source.account_name}`), + ].join(", "), + }, + { id: "pricing", header: "Pricing", accessorKey: "pricing" }, + { + id: "updated_at", + header: "Updated", + accessorKey: "updated_at", + kind: "datetime", + enableSorting: true, + }, +]; + +export function PartnerTools({ provider }: { provider?: string }) { + const query = useTableQuery("partner-products").toString(); + return ; +} + +function ProductTable({ query, provider }: { query: string; provider?: string }) { + const params = new URLSearchParams(query); + const router = useRouter(); + const q = params.get("q") ?? ""; + const sort = params.get("sort") ?? "-updated_at"; + const bounded = (value: string | null, fallback: number, max: number) => + value !== null && /^\d+$/.test(value) ? Math.min(max, Number(value)) : fallback; + const offset = bounded(params.get("offset"), 0, 100000000); + const limit = Math.max(1, bounded(params.get("limit"), 25, 100)); + const [revision, setRevision] = useState(0); + const interval = useRefreshInterval(); + const load = useCallback( + async (signal: AbortSignal) => + adminPartnerToolsList({ provider, q, sort, offset, limit }, { signal }), + [provider, q, sort, offset, limit], + ); + const result = useRequest( + `partner-products/${provider}/${query}/${revision}`, + load, + interval * 1000, + ); + function change(values: Record) { + const next = new URLSearchParams(query); + next.set("offset", "0"); + Object.entries(values).forEach(([key, value]) => + value ? next.set(key, value) : next.delete(key), + ); + router.replace(`/partnerships/products?${next}`, { scroll: false }); + } + return ( +
+ + ({ enableSorting: false, ...column }))} + getRowId={(row) => row.id} + columnChoices + loading={result.loading} + error={result.error} + onRetry={() => setRevision((n) => n + 1)} + sort={sort} + onSortChange={(sort) => change({ sort })} + pagination={{ offset, limit, total: result.data?.total ?? 0, onChange: change }} + empty={ + q + ? "No products match your search." + : "No partner products. Partners can add products directly or connect an import source." + } + toolbar={ + <> + change({ q })} + /> + + {provider && ( + + )} + + } + /> +
+ ); +} diff --git a/apps/admin/src/components/organisms/record-history.tsx b/apps/admin/src/components/organisms/record-history.tsx index 9fbe32f1b..364f6ea9d 100644 --- a/apps/admin/src/components/organisms/record-history.tsx +++ b/apps/admin/src/components/organisms/record-history.tsx @@ -4,8 +4,8 @@ import { DateTime } from "@/components/molecules/date-time"; import { useCallback, useState } from "react"; import { adminArticleReviews, adminSourceReviews } from "@/lib/api/generated/admin"; import { RequestState } from "@/components/molecules/request-state"; -import { DataValue } from "@/components/molecules/info-panel"; -import { Button } from "@/components/atoms/button"; +import { DataValue } from "@devfeed/ui/resource-layout"; +import { Button } from "@devfeed/ui/button"; import { useRequest } from "@/lib/use-request"; import type { PageArticleReviewOut, PageSourceReviewOut } from "@/lib/api/generated/models"; export function RecordHistory({ resource, id }: { resource: "articles" | "sources"; id: string }) { diff --git a/apps/admin/src/components/organisms/record-table.tsx b/apps/admin/src/components/organisms/record-table.tsx index 1f26fa5bd..88c90de78 100644 --- a/apps/admin/src/components/organisms/record-table.tsx +++ b/apps/admin/src/components/organisms/record-table.tsx @@ -6,6 +6,7 @@ import { type ReactNode } from "react"; import { DataTable, type DataTableColumn } from "@/components/molecules/data-table"; import { RecordActions } from "@/components/molecules/record-actions"; import { RecordLink } from "@/components/molecules/record-link"; +import { ImageJobSubject, imageJobOperation } from "@/components/molecules/image-job-subject"; import { relationshipLabel } from "@/components/molecules/relationship-proposal-actions"; import { type Resource, resources, recordHref } from "@/lib/resources"; import type { RecordData, RecordPage } from "@/lib/resource-api"; @@ -168,6 +169,10 @@ export function RecordTable({ enableSorting: !!column.sort, cell: ({ row }: { row: { original: RecordData } }) => { const value = row.original[column.key]; + if (resource === "image-jobs" && column.key === "target_name") + return ; + if (resource === "image-jobs" && column.key === "operation") + return imageJobOperation(row.original); if (resource === "analysis-jobs" && column.key === "target_name") { const record = row.original; if (record.proposal_id) diff --git a/apps/admin/src/components/organisms/relationship-discovery.tsx b/apps/admin/src/components/organisms/relationship-discovery.tsx index c8824c391..fa54e9f28 100644 --- a/apps/admin/src/components/organisms/relationship-discovery.tsx +++ b/apps/admin/src/components/organisms/relationship-discovery.tsx @@ -4,10 +4,10 @@ import Link from "next/link"; import { useCallback, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { Sparkles } from "lucide-react"; -import { Button } from "@/components/atoms/button"; +import { Button } from "@devfeed/ui/button"; import { Field } from "@/components/molecules/field"; import { EntityPicker } from "@/components/molecules/entity-picker"; -import { PageHeading } from "@/components/molecules/page-heading"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; import { ValidationErrors } from "@/components/molecules/validation-errors"; import { useAdmin } from "@/components/molecules/admin-session"; import { adminTopicGet, adminTopicRelationshipsAnalyze } from "@/lib/api/generated/admin"; @@ -54,7 +54,7 @@ export function RelationshipDiscovery() { } return (
- Review proposals - +
{ diff --git a/apps/admin/src/components/organisms/relationship-proposals.tsx b/apps/admin/src/components/organisms/relationship-proposals.tsx index 3550d54c2..7b3afeed2 100644 --- a/apps/admin/src/components/organisms/relationship-proposals.tsx +++ b/apps/admin/src/components/organisms/relationship-proposals.tsx @@ -7,13 +7,13 @@ import { useCallback, useState } from "react"; import { useTableQuery } from "@/lib/use-table-query"; import { useRouter } from "next/navigation"; import { Check, Sparkles, Trash2, X } from "lucide-react"; -import { Button } from "@/components/atoms/button"; +import { Button } from "@devfeed/ui/button"; import { SearchField, searchScope } from "@/components/molecules/search-field"; import { DataTable, type DataTableColumn } from "@/components/molecules/data-table"; import type { BulkAction } from "@/components/molecules/table-bulk-actions"; import { FormField } from "@/components/molecules/form-field"; import { adminRouteTitle } from "@/lib/page-titles"; -import { PageHeading } from "@/components/molecules/page-heading"; +import { ResourceHeading } from "@devfeed/ui/resource-layout"; import { RelationshipProposalActions, relationshipLabel as label, @@ -216,7 +216,7 @@ export function RelationshipProposals() { ]; return (
- - +