From 487963d24d50a532b490aab97c31e56929010d2c Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 2 Oct 2026 18:49:26 +0530 Subject: [PATCH 01/85] Add private partner catalog and article relevance evaluation --- CHANGELOG.md | 4 + apps/admin-api/src/jobs.py | 7 +- apps/admin-api/src/main.py | 2 + apps/admin-api/src/partner_tools.py | 265 + apps/admin/README.md | 46 + apps/admin/openapi.json | 4279 +++++++++++------ apps/admin/orval.config.ts | 1 + .../src/app/(admin)/partner-tools/page.tsx | 7 + .../components/organisms/partner-tools.tsx | 760 +++ .../src/components/organisms/sidebar.tsx | 14 + apps/admin/src/lib/api/generated/admin.ts | 275 +- .../api/generated/models/adminJobOutKind.ts | 1 + .../models/adminPartnerToolsListParams.ts | 24 + .../api/generated/models/articleSnapshot.ts | 16 + .../api/generated/models/evaluationInput.ts | 12 + .../lib/api/generated/models/evaluationOut.ts | 23 + .../api/generated/models/evaluationResult.ts | 13 + .../generated/models/evaluationSnapshot.ts | 15 + .../src/lib/api/generated/models/evidence.ts | 21 + .../src/lib/api/generated/models/index.ts | 26 + .../lib/api/generated/models/matchDecision.ts | 26 + .../lib/api/generated/models/matchReview.ts | 18 + .../generated/models/matchReviewDecision.ts | 15 + .../lib/api/generated/models/nickImport.ts | 16 + .../lib/api/generated/models/nickProduct.ts | 28 + .../api/generated/models/pageProductOut.ts | 15 + .../generated/models/partnerReviewEvent.ts | 20 + .../models/partnerReviewEventActor.ts | 9 + .../models/partnerReviewEventDecision.ts | 15 + .../lib/api/generated/models/productImport.ts | 16 + .../lib/api/generated/models/productInput.ts | 45 + .../generated/models/productInputPricing.ts | 17 + .../lib/api/generated/models/productOut.ts | 54 + .../api/generated/models/productOutPricing.ts | 17 + .../api/generated/models/productOutStatus.ts | 18 + .../lib/api/generated/models/productReview.ts | 21 + .../generated/models/productReviewStatus.ts | 18 + .../api/generated/models/productSnapshot.ts | 46 + .../models/productSnapshotPricing.ts | 17 + apps/admin/tests/browser/partner-tools.mjs | 222 + apps/admin/tests/partner-tools.test.tsx | 122 + apps/aggregator/src/partner_tasks.py | 118 + apps/aggregator/src/scheduler.py | 2 + apps/aggregator/src/worker.py | 1 + migrations/versions/0021_partner_tools.py | 74 + packages/core/src/job_logs.py | 1 + packages/core/src/models.py | 51 + packages/core/src/partner_tools.py | 274 ++ tests/test_partner_tools.py | 147 + tests/test_partner_tools_integration.py | 251 + 50 files changed, 6059 insertions(+), 1446 deletions(-) create mode 100644 apps/admin-api/src/partner_tools.py create mode 100644 apps/admin/src/app/(admin)/partner-tools/page.tsx create mode 100644 apps/admin/src/components/organisms/partner-tools.tsx create mode 100644 apps/admin/src/lib/api/generated/models/adminPartnerToolsListParams.ts create mode 100644 apps/admin/src/lib/api/generated/models/articleSnapshot.ts create mode 100644 apps/admin/src/lib/api/generated/models/evaluationInput.ts create mode 100644 apps/admin/src/lib/api/generated/models/evaluationOut.ts create mode 100644 apps/admin/src/lib/api/generated/models/evaluationResult.ts create mode 100644 apps/admin/src/lib/api/generated/models/evaluationSnapshot.ts create mode 100644 apps/admin/src/lib/api/generated/models/evidence.ts create mode 100644 apps/admin/src/lib/api/generated/models/matchDecision.ts create mode 100644 apps/admin/src/lib/api/generated/models/matchReview.ts create mode 100644 apps/admin/src/lib/api/generated/models/matchReviewDecision.ts create mode 100644 apps/admin/src/lib/api/generated/models/nickImport.ts create mode 100644 apps/admin/src/lib/api/generated/models/nickProduct.ts create mode 100644 apps/admin/src/lib/api/generated/models/pageProductOut.ts create mode 100644 apps/admin/src/lib/api/generated/models/partnerReviewEvent.ts create mode 100644 apps/admin/src/lib/api/generated/models/partnerReviewEventActor.ts create mode 100644 apps/admin/src/lib/api/generated/models/partnerReviewEventDecision.ts create mode 100644 apps/admin/src/lib/api/generated/models/productImport.ts create mode 100644 apps/admin/src/lib/api/generated/models/productInput.ts create mode 100644 apps/admin/src/lib/api/generated/models/productInputPricing.ts create mode 100644 apps/admin/src/lib/api/generated/models/productOut.ts create mode 100644 apps/admin/src/lib/api/generated/models/productOutPricing.ts create mode 100644 apps/admin/src/lib/api/generated/models/productOutStatus.ts create mode 100644 apps/admin/src/lib/api/generated/models/productReview.ts create mode 100644 apps/admin/src/lib/api/generated/models/productReviewStatus.ts create mode 100644 apps/admin/src/lib/api/generated/models/productSnapshot.ts create mode 100644 apps/admin/src/lib/api/generated/models/productSnapshotPricing.ts create mode 100644 apps/admin/tests/browser/partner-tools.mjs create mode 100644 apps/admin/tests/partner-tools.test.tsx create mode 100644 apps/aggregator/src/partner_tasks.py create mode 100644 migrations/versions/0021_partner_tools.py create mode 100644 packages/core/src/partner_tools.py create mode 100644 tests/test_partner_tools.py create mode 100644 tests/test_partner_tools_integration.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ea02582e6..c5782c68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,10 @@ 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 + +- Review partner tools, verify capability evidence, and privately evaluate article relevance from the admin interface before introducing reader suggestions. + ## 0.0.48 — 2026-10-02 ### Fixes diff --git a/apps/admin-api/src/jobs.py b/apps/admin-api/src/jobs.py index bd748246c..f30cd7f7b 100644 --- a/apps/admin-api/src/jobs.py +++ b/apps/admin-api/src/jobs.py @@ -13,6 +13,7 @@ ArticleAnalysisJob, ArticleImageJob, NotificationDelivery, + PartnerEvaluation, Source, SourceDiscoveryJob, Topic, @@ -35,7 +36,11 @@ 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, +} router = APIRouter( prefix="/v1/admin/jobs", tags=["admin-jobs"], dependencies=[Depends(require_admin)] ) diff --git a/apps/admin-api/src/main.py b/apps/admin-api/src/main.py index f22bd4a22..cb571bec2 100644 --- a/apps/admin-api/src/main.py +++ b/apps/admin-api/src/main.py @@ -24,6 +24,7 @@ notifications, overview, overview_panels, + partner_tools, search_analytics, search_suggestions, source_imports, @@ -103,6 +104,7 @@ def create_app() -> FastAPI: ai_connection.router, overview.router, overview_panels.router, + partner_tools.router, automation.router, taxonomy.router, topic_proposals.router, diff --git a/apps/admin-api/src/partner_tools.py b/apps/admin-api/src/partner_tools.py new file mode 100644 index 000000000..69255c188 --- /dev/null +++ b/apps/admin-api/src/partner_tools.py @@ -0,0 +1,265 @@ +"""Admin-only catalog. No public serving endpoint or automatic admission.""" + +import uuid + +from devfeed_core.config import get_settings +from devfeed_core.models import Article, PartnerEvaluation, PartnerProduct, utcnow +from devfeed_core.partner_tools import ( + EvaluationInput, + EvaluationOut, + MatchReview, + NickImport, + ProductImport, + ProductOut, + ProductReview, + article_snapshot, + eligible, + product_snapshot, + product_view, + snapshot_current, +) +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.dialects.postgresql import insert + +from devfeed_admin_api.auth import Admin, actor, require_admin +from devfeed_admin_api.dependencies import DB +from devfeed_admin_api.pagination import Listing, Page, paginate, record + +router = APIRouter( + prefix="/v1/admin/partner-tools", + tags=["admin-partner-tools"], + dependencies=[Depends(require_admin)], +) + + +def event(admin, action, note=""): + return {"actor": actor(admin), "action": action, "note": note, "at": utcnow().isoformat()} + + +def locked_product(session, identifier): + value = session.scalar( + select(PartnerProduct).where(PartnerProduct.id == identifier).with_for_update() + ) + if value is None: + raise HTTPException(404, "Product not found") + return value + + +@router.get("", response_model=Page[ProductOut], operation_id="admin_partner_tools_list") +def listing(session: DB, query: Listing): + statement = select(PartnerProduct) + if query.q: + statement = statement.where(PartnerProduct.name.ilike(f"%{query.q}%")) + page = paginate( + session, + statement, + query, + {"name": PartnerProduct.name, "updated_at": PartnerProduct.updated_at}, + "-updated_at", + ) + return {**page, "items": [product_view(p) for p in page["items"]]} + + +@router.post("/import", response_model=list[ProductOut], operation_id="admin_partner_tools_import") +def import_products(body: ProductImport, session: DB, admin: Admin): + return import_batch(body, session, admin) + + +def import_batch(body, session, admin, *, preserve_evidence=False): + identities = [(p.provider, p.external_id) for p in body.products] + if len(identities) != len(set(identities)): + raise HTTPException(422, "Duplicate product identities in import") + products = [] + # Stable lock order prevents two overlapping imports deadlocking. + for item in sorted(body.products, key=lambda p: (p.provider, p.external_id)): + values = item.model_dump(mode="json") + identifier = session.scalar( + insert(PartnerProduct) + .values(**values, reviews=[event(admin, "imported")]) + .on_conflict_do_nothing(constraint="uq_partner_identity") + .returning(PartnerProduct.id) + ) + product = session.scalar( + select(PartnerProduct) + .where( + PartnerProduct.provider == item.provider, + PartnerProduct.external_id == item.external_id, + ) + .with_for_update() + ) + assert product is not None + if identifier is None and preserve_evidence: + values["evidence"] = product.evidence + values["technologies"] = product.technologies + if identifier is None and any( + getattr(product, key) != value for key, value in values.items() + ): + for key, value in values.items(): + setattr(product, key, value) + product.revision += 1 + if product.status not in {"withdrawn", "rejected", "paused"}: + product.status = "pending" + product.verified_at = None + product.updated_at = utcnow() + product.reviews = [*product.reviews, event(admin, "updated", "Approval invalidated")] + products.append(product) + session.commit() + return [product_view(p) for p in products] + + +@router.post( + "/import/nick", response_model=list[ProductOut], operation_id="admin_partner_tools_import_nick" +) +def import_nick(body: NickImport, session: DB, admin: Admin): + from pydantic import ValidationError + + try: + products = [entry.product() for entry in body.products] + except ValidationError as exc: + raise HTTPException( + 422, "Each product needs a description or tagline of at least 10 characters" + ) from exc + # Preserve local evidence under the same product lock used for updates. + return import_batch(ProductImport(products=products), session, admin, preserve_evidence=True) + + +@router.post( + "/{product_id}/review", response_model=ProductOut, operation_id="admin_partner_tools_review" +) +def review_product(product_id: uuid.UUID, body: ProductReview, session: DB, admin: Admin): + product = locked_product(session, product_id) + if product.revision != body.expected_revision: + raise HTTPException(409, "Product changed since you opened it; refresh and review again") + if body.status == "approved" and ( + not body.evidence_checked + or not body.display_rights_confirmed + or not product.evidence + or not product.technologies + ): + raise HTTPException( + 422, + "Approval requires technologies, capability evidence, " + "checked evidence and display rights", + ) + product.status = body.status + product.verified_at = utcnow() if body.status == "approved" else None + product.revision += 1 + product.updated_at = utcnow() + product.reviews = [ + *product.reviews, + { + **event(admin, body.status, body.note), + "evidence_checked": body.evidence_checked, + "display_rights_confirmed": body.display_rights_confirmed, + }, + ] + session.commit() + return product_view(product) + + +def evaluation_view(session, job): + return { + **{ + key: getattr(job, key) + for key in ( + "id", + "product_id", + "status", + "created_at", + "finished_at", + "error", + "snapshot", + "result", + "reviews", + ) + }, + "result": job.result or None, + "current": snapshot_current(session, job), + } + + +@router.post( + "/{product_id}/evaluations", + response_model=EvaluationOut, + status_code=202, + operation_id="admin_partner_tools_evaluate", +) +def evaluate(product_id: uuid.UUID, body: EvaluationInput, session: DB, admin: Admin): + if not get_settings().ai_enabled: + raise HTTPException(409, "Enable AI to run a shadow evaluation") + product = locked_product(session, product_id) + if not eligible(product): + raise HTTPException(409, "Approve and verify this product before evaluation") + active = session.scalar( + select(PartnerEvaluation).where( + PartnerEvaluation.product_id == product_id, + PartnerEvaluation.status.in_(["queued", "running"]), + ) + ) + if active: + raise HTTPException(409, "An evaluation is already queued or running") + statement = select(Article).where(Article.publication_status == "published") + if body.article_ids: + statement = statement.where(Article.id.in_(body.article_ids)) + articles = session.scalars( + statement.order_by(Article.feed_at.desc(), Article.id).limit(20) + ).all() + if not articles or (body.article_ids and len(articles) != len(set(body.article_ids))): + raise HTTPException(422, "Choose existing published articles for evaluation") + job = PartnerEvaluation( + product_id=product.id, + requested_by=actor(admin), + snapshot={ + "version": "1", + "product": product_snapshot(product), + "articles": [article_snapshot(article) for article in articles], + }, + ) + session.add(job) + session.commit() + return evaluation_view(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): + record(session, PartnerProduct, product_id) + 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] + + +@router.post( + "/{product_id}/evaluations/{job_id}/review", + response_model=EvaluationOut, + operation_id="admin_partner_tools_match_review", +) +def review_match( + product_id: uuid.UUID, job_id: uuid.UUID, body: MatchReview, session: DB, admin: Admin +): + locked_product(session, product_id) + job = session.scalar( + select(PartnerEvaluation) + .where(PartnerEvaluation.id == job_id, PartnerEvaluation.product_id == product_id) + .with_for_update() + ) + if job is None: + raise HTTPException(404, "Evaluation not found") + if job.status != "succeeded" or not snapshot_current(session, job): + raise HTTPException(409, "Evaluation is incomplete or stale; run it again") + if not any(d["article_id"] == str(body.article_id) for d in job.result.get("decisions", [])): + raise HTTPException(422, "Article is not part of this evaluation") + job.reviews = [ + *job.reviews, + {**body.model_dump(mode="json"), **event(admin, "match_review", body.note)}, + ] + session.commit() + return evaluation_view(session, job) diff --git a/apps/admin/README.md b/apps/admin/README.md index b60a36542..5ccf12ed7 100644 --- a/apps/admin/README.md +++ b/apps/admin/README.md @@ -114,3 +114,49 @@ and verification of runtime settings, authentication, and personalized feeds. Validate with `npm run admin:lint`, `npm run admin:test`, and `npm run admin:build`. The overview browser suite checks nonce rotation after its overview scenarios so the extra page loads do not affect panel concurrency or lazy-loading measurements. + + +## Partner tools: private evaluation + +Open **Content → Partner tools** to manage the partner inventory. This feature has no +reader endpoint, feed insertion, launch page, notification, or public placement. + +1. Add a product manually, or paste a Nick Launches API `results` response (or array) + using **Import a product collection → Nick Launches API JSON**. Each import accepts + at most 50 products. Automatic network synchronization is not enabled; API access + and partner update/withdrawal delivery must be agreed before adding a scheduled importer. +2. Add specific technologies and capability evidence: an official documentation URL, + exact quote, and the capability it supports. Imported marketing categories and + popularity never count as verified evidence. Check sources manually. +3. Record a review note and confirm evidence and display permission before approval. + Approval is optimistic: if the product changed, refresh and review again. Pause, + reject, or withdraw products explicitly. Verification expires after 90 days. +4. Run a private evaluation against up to 20 published article IDs, including unrelated + examples, or leave the sample blank for the latest 20 articles. Only tutorials can + qualify in this pilot. Original publisher summaries provide the article evidence; + short or incomplete summaries should produce no match rather than inferred claims. +5. Refresh for results. Review both proposed matches and no-match results. **Agree** + confirms the assessment, including a correct negative; **Disagree** flags it for + analysis. Neither action publishes anything. The latest 10 runs are shown, with + all run snapshots and review history retained in the database. + +Generic imports use an array of objects with `provider`, `external_id`, `name`, +`product_url`, `listing_url`, `description`, `pricing` (`free`, `freemium`, `paid`, +`unknown`), `technologies`, `attribution`, and `evidence` (objects containing `url`, +`quote`, and `capability`). Provider plus external ID is the stable identity. An +identical reimport is a no-op; material changes invalidate verification. Native Nick +imports preserve local technology/evidence enrichment. Withdrawn, rejected, and paused +products stay in that state on reimport. Missing items are not treated as deletions. + +Migration `0021` adds `partner_products` and `partner_evaluations`; run the normal +migration job before updating services. The scheduler dispatches evaluations to the +existing `source-analysis` queue, which requires AI-enabled workers. Jobs use exclusive +leases, at most three attempts, bounded samples, and a product/article snapshot check +before saving results. Changed or expired evidence makes historical results stale. + +The model may propose only grounded candidates; deterministic checks reject missing +articles, fabricated article quotes, unsupported technology matches, and non-tutorial +positives. Human review is still required to establish semantic relevance. No real +partner sample has been validated simply by shipping this workflow. Reader placement, +frequency controls, advertising campaigns, billing, and behavioral targeting are not +part of this milestone. diff --git a/apps/admin/openapi.json b/apps/admin/openapi.json index 5021f87ce..a5caff517 100644 --- a/apps/admin/openapi.json +++ b/apps/admin/openapi.json @@ -589,6 +589,7 @@ "enum": [ "ingestion", "source-discovery", + "partner-evaluation", "article-enrichment", "images", "source-enrichment", @@ -2448,6 +2449,45 @@ "title": "ArticleReviewOut", "type": "object" }, + "ArticleSnapshot": { + "properties": { + "content_type": { + "title": "Content Type", + "type": "string" + }, + "editorial_revision": { + "title": "Editorial Revision", + "type": "integer" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "published": { + "title": "Published", + "type": "boolean" + }, + "text": { + "title": "Text", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "id", + "title", + "text", + "content_type", + "published", + "editorial_revision" + ], + "title": "ArticleSnapshot", + "type": "object" + }, "ArticleTopicOut": { "properties": { "evidence": { @@ -3219,6 +3259,177 @@ "title": "ErrorResponse", "type": "object" }, + "EvaluationInput": { + "additionalProperties": false, + "properties": { + "article_ids": { + "items": { + "format": "uuid", + "type": "string" + }, + "maxItems": 20, + "title": "Article Ids", + "type": "array" + } + }, + "title": "EvaluationInput", + "type": "object" + }, + "EvaluationOut": { + "properties": { + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "current": { + "title": "Current", + "type": "boolean" + }, + "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" + }, + "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", + "product_id", + "status", + "created_at", + "finished_at", + "error", + "snapshot", + "result", + "reviews", + "current" + ], + "title": "EvaluationOut", + "type": "object" + }, + "EvaluationResult": { + "additionalProperties": false, + "properties": { + "decisions": { + "items": { + "$ref": "#/components/schemas/MatchDecision" + }, + "maxItems": 20, + "title": "Decisions", + "type": "array" + } + }, + "required": [ + "decisions" + ], + "title": "EvaluationResult", + "type": "object" + }, + "EvaluationSnapshot": { + "properties": { + "articles": { + "items": { + "$ref": "#/components/schemas/ArticleSnapshot" + }, + "title": "Articles", + "type": "array" + }, + "product": { + "$ref": "#/components/schemas/ProductSnapshot" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version", + "product", + "articles" + ], + "title": "EvaluationSnapshot", + "type": "object" + }, + "Evidence": { + "additionalProperties": false, + "properties": { + "capability": { + "maxLength": 500, + "minLength": 10, + "title": "Capability", + "type": "string" + }, + "quote": { + "maxLength": 2000, + "minLength": 20, + "title": "Quote", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url", + "quote", + "capability" + ], + "title": "Evidence", + "type": "object" + }, "FeedSettings": { "additionalProperties": false, "properties": { @@ -4462,80 +4673,226 @@ "title": "LabelSelection", "type": "object" }, - "NotificationConfig": { + "MatchDecision": { + "additionalProperties": false, "properties": { - "enabled": { - "title": "Enabled", - "type": "boolean" + "article_id": { + "format": "uuid", + "title": "Article Id", + "type": "string" }, - "environment": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Environment" + "article_quote": { + "maxLength": 1000, + "title": "Article Quote", + "type": "string" }, - "subscriber_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Subscriber 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": [ - "enabled" + "article_id", + "relevant", + "reason", + "article_quote", + "evidence_index", + "technology" ], - "title": "NotificationConfig", + "title": "MatchDecision", "type": "object" }, - "NotificationSettings": { + "MatchReview": { "additionalProperties": false, "properties": { - "show_badge": { - "default": true, - "title": "Show Badge", - "type": "boolean" - }, - "sound": { - "default": false, - "title": "Sound", - "type": "boolean" - } - }, - "title": "NotificationSettings", - "type": "object" - }, - "OverviewActivity": { - "properties": { - "added": { - "title": "Added", - "type": "integer" + "article_id": { + "format": "uuid", + "title": "Article Id", + "type": "string" }, - "date": { - "format": "date", - "title": "Date", + "decision": { + "enum": [ + "accepted", + "rejected" + ], + "title": "Decision", "type": "string" }, - "published": { - "title": "Published", - "type": "integer" + "note": { + "maxLength": 2000, + "minLength": 10, + "title": "Note", + "type": "string" } }, "required": [ - "date", - "added", - "published" + "article_id", + "decision", + "note" ], - "title": "OverviewActivity", + "title": "MatchReview", + "type": "object" + }, + "NickImport": { + "additionalProperties": false, + "properties": { + "products": { + "items": { + "$ref": "#/components/schemas/NickProduct" + }, + "maxItems": 50, + "minItems": 1, + "title": "Products", + "type": "array" + } + }, + "required": [ + "products" + ], + "title": "NickImport", + "type": "object" + }, + "NickProduct": { + "properties": { + "description": { + "default": "", + "maxLength": 5000, + "title": "Description", + "type": "string" + }, + "name": { + "maxLength": 200, + "minLength": 1, + "title": "Name", + "type": "string" + }, + "pricing": { + "default": "unknown", + "maxLength": 100, + "title": "Pricing", + "type": "string" + }, + "productUrl": { + "title": "Producturl", + "type": "string" + }, + "slug": { + "maxLength": 200, + "minLength": 1, + "title": "Slug", + "type": "string" + }, + "tagline": { + "default": "", + "maxLength": 5000, + "title": "Tagline", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "slug", + "name", + "productUrl", + "url" + ], + "title": "NickProduct", + "type": "object" + }, + "NotificationConfig": { + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment" + }, + "subscriber_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Subscriber Id" + } + }, + "required": [ + "enabled" + ], + "title": "NotificationConfig", + "type": "object" + }, + "NotificationSettings": { + "additionalProperties": false, + "properties": { + "show_badge": { + "default": true, + "title": "Show Badge", + "type": "boolean" + }, + "sound": { + "default": false, + "title": "Sound", + "type": "boolean" + } + }, + "title": "NotificationSettings", + "type": "object" + }, + "OverviewActivity": { + "properties": { + "added": { + "title": "Added", + "type": "integer" + }, + "date": { + "format": "date", + "title": "Date", + "type": "string" + }, + "published": { + "title": "Published", + "type": "integer" + } + }, + "required": [ + "date", + "added", + "published" + ], + "title": "OverviewActivity", "type": "object" }, "OverviewAdoption": { @@ -5733,6 +6090,37 @@ "title": "Page[ArticleReviewOut]", "type": "object" }, + "Page_ProductOut_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ProductOut" + }, + "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[ProductOut]", + "type": "object" + }, "Page_PublicationDecisionOut_": { "properties": { "items": { @@ -6043,6 +6431,87 @@ "title": "Page[TopicReplacementOut]", "type": "object" }, + "PartnerReviewEvent": { + "properties": { + "action": { + "title": "Action", + "type": "string" + }, + "actor": { + "additionalProperties": { + "type": "string" + }, + "title": "Actor", + "type": "object" + }, + "article_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Article Id" + }, + "at": { + "format": "date-time", + "title": "At", + "type": "string" + }, + "decision": { + "anyOf": [ + { + "enum": [ + "accepted", + "rejected" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Decision" + }, + "display_rights_confirmed": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Display Rights Confirmed" + }, + "evidence_checked": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Evidence Checked" + }, + "note": { + "title": "Note", + "type": "string" + } + }, + "required": [ + "actor", + "action", + "note", + "at" + ], + "title": "PartnerReviewEvent", + "type": "object" + }, "PipelineHour": { "properties": { "article_analyses": { @@ -6277,103 +6746,138 @@ "title": "PipelineThroughput", "type": "object" }, - "ProfileLink": { + "ProductImport": { "additionalProperties": false, "properties": { - "label": { - "anyOf": [ - { - "maxLength": 80, - "pattern": "^[^\\x00]*$", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Label" - }, - "url": { - "maxLength": 2048, - "title": "Url", - "type": "string" + "products": { + "items": { + "$ref": "#/components/schemas/ProductInput" + }, + "maxItems": 50, + "minItems": 1, + "title": "Products", + "type": "array" } }, "required": [ - "url" + "products" ], - "title": "ProfileLink", + "title": "ProductImport", "type": "object" }, - "ProfileSettings": { + "ProductInput": { "additionalProperties": false, "properties": { - "avatar_url": { - "anyOf": [ - { - "maxLength": 2048, - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Avatar Url" + "attribution": { + "default": "", + "maxLength": 300, + "title": "Attribution", + "type": "string" }, - "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", + "description": { + "maxLength": 5000, + "minLength": 10, + "title": "Description", "type": "string" }, - "decision": { - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" + "evidence": { + "items": { + "$ref": "#/components/schemas/Evidence" }, - "title": "Decision", - "type": "object" + "maxItems": 10, + "title": "Evidence", + "type": "array" }, - "id": { - "format": "uuid", - "title": "Id", + "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": "Pricing", + "type": "string" + }, + "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": [ - "id", - "created_at", - "decision" + "provider", + "external_id", + "name", + "product_url", + "listing_url", + "description" ], - "title": "PublicationDecisionOut", + "title": "ProductInput", "type": "object" }, - "PublicationPolicyReviewOut": { + "ProductOut": { + "additionalProperties": false, "properties": { - "actor": { - "title": "Actor", + "attribution": { + "default": "", + "maxLength": 300, + "title": "Attribution", "type": "string" }, - "created_at": { - "format": "date-time", - "title": "Created At", + "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" + }, + "external_id": { + "maxLength": 200, + "minLength": 1, + "title": "External Id", "type": "string" }, "id": { @@ -6381,115 +6885,449 @@ "title": "Id", "type": "string" }, - "mode": { - "title": "Mode", + "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": "Pricing", + "type": "string" + }, + "product_url": { + "title": "Product Url", + "type": "string" + }, + "provider": { + "maxLength": 200, + "minLength": 1, + "title": "Provider", "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": [ + "provider", + "external_id", + "name", + "product_url", + "listing_url", + "description", "id", - "created_at", - "mode", "revision", - "actor" + "status", + "verified_at", + "updated_at", + "reviews", + "eligible" ], - "title": "PublicationPolicyReviewOut", + "title": "ProductOut", "type": "object" }, - "PublicationPolicyUpdate": { + "ProductReview": { "additionalProperties": false, "properties": { + "display_rights_confirmed": { + "default": false, + "title": "Display Rights Confirmed", + "type": "boolean" + }, + "evidence_checked": { + "default": false, + "title": "Evidence Checked", + "type": "boolean" + }, "expected_revision": { - "minimum": 0.0, + "minimum": 1.0, "title": "Expected Revision", "type": "integer" }, - "mode": { + "note": { + "maxLength": 2000, + "minLength": 10, + "title": "Note", + "type": "string" + }, + "status": { "enum": [ - "manual", - "preview", - "auto" + "pending", + "approved", + "rejected", + "paused", + "withdrawn" ], - "title": "Mode", + "title": "Status", "type": "string" } }, "required": [ - "mode", - "expected_revision" + "expected_revision", + "status", + "note" ], - "title": "PublicationPolicyUpdate", + "title": "ProductReview", "type": "object" }, - "Quality": { + "ProductSnapshot": { "additionalProperties": false, "properties": { - "confidence": { - "maximum": 1.0, - "minimum": 0.0, - "title": "Confidence", - "type": "number" + "attribution": { + "default": "", + "maxLength": 300, + "title": "Attribution", + "type": "string" }, - "entries": { + "description": { + "maxLength": 5000, + "minLength": 10, + "title": "Description", + "type": "string" + }, + "evidence": { "items": { - "$ref": "#/components/schemas/EntryQuality" + "$ref": "#/components/schemas/Evidence" }, "maxItems": 10, - "minItems": 1, - "title": "Entries", + "title": "Evidence", "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" + "external_id": { + "maxLength": 200, + "minLength": 1, + "title": "External Id", + "type": "string" }, - "idle_workers": { - "title": "Idle Workers", - "type": "integer" + "listing_url": { + "title": "Listing Url", + "type": "string" }, "name": { + "maxLength": 200, + "minLength": 1, "title": "Name", "type": "string" }, - "oldest_queued_at": { - "anyOf": [ - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } + "pricing": { + "default": "unknown", + "enum": [ + "free", + "freemium", + "paid", + "unknown" ], - "title": "Oldest Queued At" + "title": "Pricing", + "type": "string" }, - "queued": { - "default": 0, - "title": "Queued", - "type": "integer" + "product_url": { + "title": "Product Url", + "type": "string" + }, + "provider": { + "maxLength": 200, + "minLength": 1, + "title": "Provider", + "type": "string" + }, + "revision": { + "title": "Revision", + "type": "integer" + }, + "technologies": { + "items": { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + "maxItems": 20, + "title": "Technologies", + "type": "array" + } + }, + "required": [ + "provider", + "external_id", + "name", + "product_url", + "listing_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", @@ -9788,56 +10626,486 @@ "title": "Generated At", "type": "string" }, - "queues": { - "items": { - "$ref": "#/components/schemas/QueueOut" - }, - "title": "Queues", - "type": "array" + "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" + } + }, + "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.51" + }, + "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" + } }, - "workers": { - "items": { - "$ref": "#/components/schemas/WorkerOut" - }, - "title": "Workers", - "type": "array" + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "maximum": 9223372036854775807, + "minimum": 0, + "title": "Offset", + "type": "integer" + } } - }, - "required": [ - "generated_at", - "workers", - "queues", - "ai_cooldown_seconds" ], - "title": "WorkersSnapshot", - "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.51" - }, - "openapi": "3.1.0", - "paths": { - "/health/live": { - "get": { - "operationId": "live_health_live_get", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HealthResponse" + "$ref": "#/components/schemas/Page_AdminArticleOut_" } } }, @@ -9874,21 +11142,34 @@ "description": "Service or dependency unavailable" } }, - "summary": "Live", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Articles", "tags": [ - "health" + "admin-articles" ] - } - }, - "/health/ready": { - "get": { - "operationId": "ready_health_ready_get", + }, + "post": { + "operationId": "admin_article_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleCreate" + } + } + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HealthResponse" + "$ref": "#/components/schemas/AdminArticleOut" } } }, @@ -9914,15 +11195,66 @@ }, "description": "Invalid or unauthorized request" }, - "503": { + "5XX": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnhealthyResponse" + "$ref": "#/components/schemas/ErrorResponse" } } }, - "description": "Not ready" + "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": { @@ -9935,21 +11267,36 @@ "description": "Service or dependency unavailable" } }, - "summary": "Ready", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Remove", "tags": [ - "health" + "admin-articles" ] - } - }, - "/v1/admin/ai/connection": { + }, "get": { - "operationId": "admin_ai_connection", + "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/CodexStatus" + "$ref": "#/components/schemas/AdminArticleOut" } } }, @@ -9991,21 +11338,41 @@ "APIKeyCookie": [] } ], - "summary": "Status", + "summary": "Detail", "tags": [ - "admin-ai-connection" + "admin-articles" ] - } - }, - "/v1/admin/ai/connection/login": { - "post": { - "operationId": "admin_ai_login", + }, + "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/DeviceLogin" + "$ref": "#/components/schemas/AdminArticleOut" } } }, @@ -10047,27 +11414,46 @@ "APIKeyCookie": [] } ], - "summary": "Login", + "summary": "Update", "tags": [ - "admin-ai-connection" + "admin-articles" ] } }, - "/v1/admin/ai/connection/login/cancel": { + "/v1/admin/articles/{article_id}/classify": { "post": { - "operationId": "admin_ai_login_cancel", + "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/CancelLogin" + "$ref": "#/components/schemas/ClassifyArticle" } } }, "required": true }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminArticleOut" + } + } + }, "description": "Successful Response" }, "422": { @@ -10106,160 +11492,25 @@ "APIKeyCookie": [] } ], - "summary": "Cancel", + "summary": "Classify", "tags": [ - "admin-ai-connection" + "admin-articles" ] } }, - "/v1/admin/articles": { + "/v1/admin/articles/{article_id}/content": { "get": { - "operationId": "admin_articles_list", + "operationId": "admin_article_content", "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, + "in": "path", + "name": "article_id", + "required": true, "schema": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", + "format": "uuid", + "title": "Article 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": { @@ -10267,7 +11518,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_AdminArticleOut_" + "anyOf": [ + { + "$ref": "#/components/schemas/ArticleContentOut" + }, + { + "type": "null" + } + ], + "title": "Response Admin Article Content" } } }, @@ -10309,25 +11568,39 @@ "APIKeyCookie": [] } ], - "summary": "Articles", + "summary": "Content", "tags": [ "admin-articles" ] - }, + } + }, + "/v1/admin/articles/{article_id}/review": { "post": { - "operationId": "admin_article_create", + "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/AdminArticleCreate" + "$ref": "#/components/schemas/ReviewArticle" } } }, "required": true }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { @@ -10373,15 +11646,15 @@ "APIKeyCookie": [] } ], - "summary": "Create", + "summary": "Review", "tags": [ "admin-articles" ] } }, - "/v1/admin/articles/{article_id}": { - "delete": { - "operationId": "admin_article_delete", + "/v1/admin/articles/{article_id}/reviews": { + "get": { + "operationId": "admin_article_reviews", "parameters": [ { "in": "path", @@ -10392,10 +11665,70 @@ "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": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_ArticleReviewOut_" + } + } + }, "description": "Successful Response" }, "422": { @@ -10434,34 +11767,83 @@ "APIKeyCookie": [] } ], - "summary": "Remove", + "summary": "Reviews", "tags": [ "admin-articles" ] - }, + } + }, + "/v1/admin/auth/callback": { "get": { - "operationId": "admin_article_get", + "operationId": "admin_auth_callback", "parameters": [ { - "in": "path", - "name": "article_id", - "required": true, + "in": "query", + "name": "state", + "required": false, "schema": { - "format": "uuid", - "title": "Article Id", - "type": "string" + "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": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminArticleOut" - } - } - }, + "302": { "description": "Successful Response" }, "422": { @@ -10495,46 +11877,21 @@ "description": "Service or dependency unavailable" } }, - "security": [ - { - "APIKeyCookie": [] - } - ], - "summary": "Detail", + "summary": "Callback", "tags": [ - "admin-articles" + "admin-auth" ] - }, - "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 - }, + } + }, + "/v1/admin/auth/config": { + "get": { + "operationId": "admin_auth_config", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminArticleOut" + "$ref": "#/components/schemas/AuthConfig" } } }, @@ -10571,51 +11928,49 @@ "description": "Service or dependency unavailable" } }, - "security": [ - { - "APIKeyCookie": [] - } - ], - "summary": "Update", + "summary": "Config", "tags": [ - "admin-articles" + "admin-auth" ] } }, - "/v1/admin/articles/{article_id}/classify": { - "post": { - "operationId": "admin_article_classify", + "/v1/admin/auth/login": { + "get": { + "operationId": "admin_auth_login", "parameters": [ { - "in": "path", - "name": "article_id", - "required": true, + "in": "query", + "name": "reauthenticate", + "required": false, "schema": { - "format": "uuid", - "title": "Article Id", - "type": "string" + "default": false, + "title": "Reauthenticate", + "type": "boolean" + } + }, + { + "in": "query", + "name": "provider", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "github", + "google" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClassifyArticle" - } - } - }, - "required": true - }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdminArticleOut" - } - } - }, + "302": { "description": "Successful Response" }, "422": { @@ -10649,49 +12004,17 @@ "description": "Service or dependency unavailable" } }, - "security": [ - { - "APIKeyCookie": [] - } - ], - "summary": "Classify", + "summary": "Login", "tags": [ - "admin-articles" + "admin-auth" ] } }, - "/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" - } - } - ], + "/v1/admin/auth/logout": { + "post": { + "operationId": "admin_auth_logout", "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ArticleContentOut" - }, - { - "type": "null" - } - ], - "title": "Response Admin Article Content" - } - } - }, + "204": { "description": "Successful Response" }, "422": { @@ -10730,43 +12053,21 @@ "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 - }, + "summary": "Logout", + "tags": [ + "admin-auth" + ] + } + }, + "/v1/admin/auth/me": { + "get": { + "operationId": "admin_auth_me", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminArticleOut" + "$ref": "#/components/schemas/AdminIdentity" } } }, @@ -10808,15 +12109,15 @@ "APIKeyCookie": [] } ], - "summary": "Review", + "summary": "Me", "tags": [ - "admin-articles" + "admin-auth" ] } }, - "/v1/admin/articles/{article_id}/reviews": { + "/v1/admin/automation/articles/{article_id}/decisions": { "get": { - "operationId": "admin_article_reviews", + "operationId": "admin_publication_decisions", "parameters": [ { "in": "path", @@ -10887,7 +12188,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_ArticleReviewOut_" + "$ref": "#/components/schemas/Page_PublicationDecisionOut_" } } }, @@ -10929,83 +12230,60 @@ "APIKeyCookie": [] } ], - "summary": "Reviews", + "summary": "Publication Decisions", "tags": [ - "admin-articles" + "admin-automation" ] } }, - "/v1/admin/auth/callback": { - "get": { - "operationId": "admin_auth_callback", + "/v1/admin/automation/articles/{article_id}/{action}": { + "post": { + "operationId": "admin_automation_recover", "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, + "in": "path", + "name": "article_id", + "required": true, "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error" + "format": "uuid", + "title": "Article Id", + "type": "string" } }, { - "in": "query", - "name": "iss", - "required": false, + "in": "path", + "name": "action", + "required": true, "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } + "enum": [ + "enrich", + "analyze", + "evaluate" ], - "title": "Iss" + "title": "Action", + "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecoveryRequest" + } + } + }, + "required": true + }, "responses": { - "302": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecoveryResult" + } + } + }, "description": "Successful Response" }, "422": { @@ -11039,21 +12317,91 @@ "description": "Service or dependency unavailable" } }, - "summary": "Callback", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Recover", "tags": [ - "admin-auth" + "admin-automation" ] } }, - "/v1/admin/auth/config": { + "/v1/admin/automation/sources/{source_id}/policies": { "get": { - "operationId": "admin_auth_config", + "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/AuthConfig" + "$ref": "#/components/schemas/Page_PublicationPolicyReviewOut_" } } }, @@ -11090,36 +12438,50 @@ "description": "Service or dependency unavailable" } }, - "summary": "Config", + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Publication Policy History", "tags": [ - "admin-auth" + "admin-automation" ] } }, - "/v1/admin/auth/login": { + "/v1/admin/ingestion/article-jobs": { "get": { - "operationId": "admin_auth_login", + "operationId": "article_jobs_v1_admin_ingestion_article_jobs_get", "parameters": [ { "in": "query", - "name": "reauthenticate", + "name": "article_id", "required": false, "schema": { - "default": false, - "title": "Reauthenticate", - "type": "boolean" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Article Id" } }, { "in": "query", - "name": "provider", + "name": "status", "required": false, "schema": { "anyOf": [ { "enum": [ - "github", - "google" + "queued", + "running", + "succeeded", + "failed" ], "type": "string" }, @@ -11127,56 +12489,46 @@ "type": "null" } ], - "title": "Provider" + "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": { - "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": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "items": { + "$ref": "#/components/schemas/ArticleEnrichmentJobOut" + }, + "title": "Response Article Jobs V1 Admin Ingestion Article Jobs Get", + "type": "array" } } }, - "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": { @@ -11215,21 +12567,33 @@ "APIKeyCookie": [] } ], - "summary": "Logout", + "summary": "Article Jobs", "tags": [ - "admin-auth" + "admin-ingestion" ] } }, - "/v1/admin/auth/me": { + "/v1/admin/ingestion/article-jobs/{job_id}": { "get": { - "operationId": "admin_auth_me", + "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/AdminIdentity" + "$ref": "#/components/schemas/ArticleEnrichmentJobOut" } } }, @@ -11271,53 +12635,53 @@ "APIKeyCookie": [] } ], - "summary": "Me", + "summary": "Article Job Detail", "tags": [ - "admin-auth" + "admin-ingestion" ] } }, - "/v1/admin/automation/articles/{article_id}/decisions": { + "/v1/admin/ingestion/jobs": { "get": { - "operationId": "admin_publication_decisions", + "operationId": "jobs_v1_admin_ingestion_jobs_get", "parameters": [ - { - "in": "path", - "name": "article_id", - "required": true, - "schema": { - "format": "uuid", - "title": "Article Id", - "type": "string" - } - }, { "in": "query", - "name": "q", + "name": "source_id", "required": false, "schema": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" } }, { "in": "query", - "name": "sort", + "name": "status", "required": false, "schema": { "anyOf": [ { - "maxLength": 50, + "enum": [ + "queued", + "running", + "succeeded", + "failed" + ], "type": "string" }, { "type": "null" } ], - "title": "Sort" + "title": "Status" } }, { @@ -11325,7 +12689,7 @@ "name": "limit", "required": false, "schema": { - "default": 25, + "default": 50, "maximum": 100, "minimum": 1, "title": "Limit", @@ -11338,7 +12702,6 @@ "required": false, "schema": { "default": 0, - "maximum": 9223372036854775807, "minimum": 0, "title": "Offset", "type": "integer" @@ -11350,7 +12713,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_PublicationDecisionOut_" + "items": { + "$ref": "#/components/schemas/JobOut" + }, + "title": "Response Jobs V1 Admin Ingestion Jobs Get", + "type": "array" } } }, @@ -11392,57 +12759,33 @@ "APIKeyCookie": [] } ], - "summary": "Publication Decisions", + "summary": "Jobs", "tags": [ - "admin-automation" + "admin-ingestion" ] } }, - "/v1/admin/automation/articles/{article_id}/{action}": { - "post": { - "operationId": "admin_automation_recover", + "/v1/admin/ingestion/jobs/{job_id}": { + "get": { + "operationId": "job_detail_v1_admin_ingestion_jobs__job_id__get", "parameters": [ { "in": "path", - "name": "article_id", + "name": "job_id", "required": true, "schema": { "format": "uuid", - "title": "Article Id", - "type": "string" - } - }, - { - "in": "path", - "name": "action", - "required": true, - "schema": { - "enum": [ - "enrich", - "analyze", - "evaluate" - ], - "title": "Action", + "title": "Job Id", "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecoveryRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RecoveryResult" + "$ref": "#/components/schemas/JobOut" } } }, @@ -11465,105 +12808,40 @@ "$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" - } + }, + "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": "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/Page_PublicationPolicyReviewOut_" + "$ref": "#/components/schemas/IngestionStatus" } } }, @@ -11605,16 +12883,60 @@ "APIKeyCookie": [] } ], - "summary": "Publication Policy History", + "summary": "Status", "tags": [ - "admin-automation" + "admin-ingestion" ] } }, - "/v1/admin/ingestion/article-jobs": { + "/v1/admin/jobs/ai-analysis": { "get": { - "operationId": "article_jobs_v1_admin_ingestion_article_jobs_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", @@ -11634,24 +12956,75 @@ }, { "in": "query", - "name": "status", + "name": "proposal_id", "required": false, "schema": { "anyOf": [ { - "enum": [ - "queued", - "running", - "succeeded", - "failed" - ], + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Status" + "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" } }, { @@ -11659,7 +13032,7 @@ "name": "limit", "required": false, "schema": { - "default": 50, + "default": 25, "maximum": 100, "minimum": 1, "title": "Limit", @@ -11672,6 +13045,7 @@ "required": false, "schema": { "default": 0, + "maximum": 9223372036854775807, "minimum": 0, "title": "Offset", "type": "integer" @@ -11683,11 +13057,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_AdminJobOut_" } } }, @@ -11729,84 +13099,59 @@ "APIKeyCookie": [] } ], - "summary": "Article Jobs", + "summary": "Ai Analysis Jobs", "tags": [ - "admin-ingestion" + "admin-jobs" ] } }, - "/v1/admin/ingestion/article-jobs/{job_id}": { + "/v1/admin/jobs/{kind}": { "get": { - "operationId": "article_job_detail_v1_admin_ingestion_article_jobs__job_id__get", + "operationId": "admin_jobs_list", "parameters": [ { "in": "path", - "name": "job_id", + "name": "kind", "required": true, "schema": { - "format": "uuid", - "title": "Job Id", + "enum": [ + "ingestion", + "source-discovery", + "partner-evaluation", + "article-enrichment", + "images", + "source-enrichment", + "analysis", + "topic-analysis", + "notifications" + ], + "title": "Kind", "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": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "queued", + "running", + "succeeded", + "failed", + "retried" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, { "in": "query", "name": "source_id", @@ -11826,24 +13171,75 @@ }, { "in": "query", - "name": "status", + "name": "article_id", "required": false, "schema": { "anyOf": [ { - "enum": [ - "queued", - "running", - "succeeded", - "failed" - ], + "format": "uuid", "type": "string" }, { "type": "null" } ], - "title": "Status" + "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" } }, { @@ -11851,7 +13247,7 @@ "name": "limit", "required": false, "schema": { - "default": 50, + "default": 25, "maximum": 100, "minimum": 1, "title": "Limit", @@ -11864,6 +13260,7 @@ "required": false, "schema": { "default": 0, + "maximum": 9223372036854775807, "minimum": 0, "title": "Offset", "type": "integer" @@ -11875,11 +13272,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/JobOut" - }, - "title": "Response Jobs V1 Admin Ingestion Jobs Get", - "type": "array" + "$ref": "#/components/schemas/Page_AdminJobOut_" } } }, @@ -11923,14 +13316,34 @@ ], "summary": "Jobs", "tags": [ - "admin-ingestion" + "admin-jobs" ] } }, - "/v1/admin/ingestion/jobs/{job_id}": { + "/v1/admin/jobs/{kind}/{job_id}": { "get": { - "operationId": "job_detail_v1_admin_ingestion_jobs__job_id__get", + "operationId": "admin_job_get", "parameters": [ + { + "in": "path", + "name": "kind", + "required": true, + "schema": { + "enum": [ + "ingestion", + "source-discovery", + "partner-evaluation", + "article-enrichment", + "images", + "source-enrichment", + "analysis", + "topic-analysis", + "notifications" + ], + "title": "Kind", + "type": "string" + } + }, { "in": "path", "name": "job_id", @@ -11947,63 +13360,7 @@ "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" + "$ref": "#/components/schemas/AdminJobOut" } } }, @@ -12045,148 +13402,61 @@ "APIKeyCookie": [] } ], - "summary": "Status", + "summary": "Detail", "tags": [ - "admin-ingestion" + "admin-jobs" ] } }, - "/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" - } - }, + "/v1/admin/jobs/{kind}/{job_id}/logs": { + "get": { + "operationId": "admin_job_logs", + "parameters": [ { - "in": "query", - "name": "topic_id", - "required": false, + "in": "path", + "name": "kind", + "required": true, "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } + "enum": [ + "ingestion", + "source-discovery", + "partner-evaluation", + "article-enrichment", + "images", + "source-enrichment", + "analysis", + "topic-analysis", + "notifications" ], - "title": "Topic Id" - } - }, - { - "in": "query", - "name": "retryable_only", - "required": false, - "schema": { - "default": false, - "title": "Retryable Only", - "type": "boolean" + "title": "Kind", + "type": "string" } }, { - "in": "query", - "name": "q", - "required": false, + "in": "path", + "name": "job_id", + "required": true, "schema": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", + "format": "uuid", + "title": "Job Id", "type": "string" } }, { "in": "query", - "name": "sort", + "name": "after", "required": false, "schema": { "anyOf": [ { - "maxLength": 50, + "maxLength": 41, "type": "string" }, { "type": "null" } ], - "title": "Sort" + "title": "After" } }, { @@ -12194,24 +13464,12 @@ "name": "limit", "required": false, "schema": { - "default": 25, - "maximum": 100, + "default": 100, + "maximum": 500, "minimum": 1, "title": "Limit", "type": "integer" } - }, - { - "in": "query", - "name": "offset", - "required": false, - "schema": { - "default": 0, - "maximum": 9223372036854775807, - "minimum": 0, - "title": "Offset", - "type": "integer" - } } ], "responses": { @@ -12219,12 +13477,22 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_AdminJobOut_" + "$ref": "#/components/schemas/AdminJobLogs" } } }, "description": "Successful Response" }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Job not found" + }, "422": { "content": { "application/json": { @@ -12245,6 +13513,16 @@ }, "description": "Invalid or unauthorized request" }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Log storage unavailable" + }, "5XX": { "content": { "application/json": { @@ -12261,15 +13539,15 @@ "APIKeyCookie": [] } ], - "summary": "Ai Analysis Jobs", + "summary": "Runtime Logs", "tags": [ "admin-jobs" ] } }, - "/v1/admin/jobs/{kind}": { - "get": { - "operationId": "admin_jobs_list", + "/v1/admin/jobs/{kind}/{job_id}/retry": { + "post": { + "operationId": "admin_job_retry", "parameters": [ { "in": "path", @@ -12279,6 +13557,7 @@ "enum": [ "ingestion", "source-discovery", + "partner-evaluation", "article-enrichment", "images", "source-enrichment", @@ -12291,139 +13570,206 @@ } }, { - "in": "query", - "name": "status", - "required": false, + "in": "path", + "name": "job_id", + "required": true, "schema": { - "anyOf": [ - { - "enum": [ - "queued", - "running", - "succeeded", - "failed", - "retried" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" + "format": "uuid", + "title": "Job Id", + "type": "string" } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminJobOut" + } + } + }, + "description": "Successful Response" }, - { - "in": "query", - "name": "source_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } - ], - "title": "Source Id" - } + } + }, + "description": "Request validation failed" }, - { - "in": "query", - "name": "article_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } - ], - "title": "Article Id" - } + } + }, + "description": "Invalid or unauthorized request" }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service or dependency unavailable" + } + }, + "security": [ { - "in": "query", - "name": "topic_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" + "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" } - ], - "title": "Topic Id" - } + } + }, + "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, + "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": { - "default": "", - "maxLength": 200, - "pattern": "^[^\\x00]*$", - "title": "Q", + "format": "uuid", + "title": "Identifier", "type": "string" } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminJobOut" + } + } + }, + "description": "Successful Response" }, - { - "in": "query", - "name": "sort", - "required": false, - "schema": { - "anyOf": [ - { - "maxLength": 50, - "type": "string" - }, - { - "type": "null" + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } - ], - "title": "Sort" - } + } + }, + "description": "Request validation failed" }, - { - "in": "query", - "name": "limit", - "required": false, - "schema": { - "default": 25, - "maximum": 100, - "minimum": 1, - "title": "Limit", - "type": "integer" - } + "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": "offset", + "name": "days", "required": false, "schema": { - "default": 0, - "maximum": 9223372036854775807, - "minimum": 0, - "title": "Offset", + "default": 30, + "maximum": 90, + "minimum": 1, + "title": "Days", "type": "integer" } } @@ -12433,7 +13779,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_AdminJobOut_" + "$ref": "#/components/schemas/AdminOverview" } } }, @@ -12475,43 +13821,70 @@ "APIKeyCookie": [] } ], - "summary": "Jobs", + "summary": "Overview", "tags": [ - "admin-jobs" + "admin-overview" ] } }, - "/v1/admin/jobs/{kind}/{job_id}": { + "/v1/admin/overview/panels/{panel}": { "get": { - "operationId": "admin_job_get", + "operationId": "admin_overview_panel", "parameters": [ { "in": "path", - "name": "kind", + "name": "panel", "required": true, "schema": { "enum": [ - "ingestion", - "source-discovery", - "article-enrichment", - "images", - "source-enrichment", - "analysis", - "topic-analysis", - "notifications" + "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": "Kind", + "title": "Panel", "type": "string" } }, { - "in": "path", - "name": "job_id", - "required": true, + "in": "query", + "name": "days", + "required": false, "schema": { - "format": "uuid", - "title": "Job Id", - "type": "string" + "default": 30, + "maximum": 90, + "minimum": 1, + "title": "Days", + "type": "integer" } } ], @@ -12520,7 +13893,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminJobOut" + "$ref": "#/components/schemas/OverviewPanel" } } }, @@ -12562,60 +13935,42 @@ "APIKeyCookie": [] } ], - "summary": "Detail", + "summary": "Overview Panel", "tags": [ - "admin-jobs" + "admin-overview" ] } }, - "/v1/admin/jobs/{kind}/{job_id}/logs": { + "/v1/admin/partner-tools": { "get": { - "operationId": "admin_job_logs", + "operationId": "admin_partner_tools_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": "path", - "name": "job_id", - "required": true, + "in": "query", + "name": "q", + "required": false, "schema": { - "format": "uuid", - "title": "Job Id", + "default": "", + "maxLength": 200, + "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" } }, { @@ -12623,12 +13978,23 @@ "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, + "minimum": 0, + "title": "Offset", + "type": "integer" + } } ], "responses": { @@ -12636,13 +14002,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminJobLogs" + "$ref": "#/components/schemas/Page_ProductOut_" } } }, "description": "Successful Response" }, - "404": { + "422": { "content": { "application/json": { "schema": { @@ -12650,9 +14016,9 @@ } } }, - "description": "Job not found" + "description": "Request validation failed" }, - "422": { + "4XX": { "content": { "application/json": { "schema": { @@ -12660,9 +14026,9 @@ } } }, - "description": "Request validation failed" + "description": "Invalid or unauthorized request" }, - "4XX": { + "5XX": { "content": { "application/json": { "schema": { @@ -12670,9 +14036,49 @@ } } }, - "description": "Invalid or unauthorized request" + "description": "Service or dependency unavailable" + } + }, + "security": [ + { + "APIKeyCookie": [] + } + ], + "summary": "Listing", + "tags": [ + "admin-partner-tools" + ] + } + }, + "/v1/admin/partner-tools/import": { + "post": { + "operationId": "admin_partner_tools_import", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductImport" + } + } }, - "503": { + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ProductOut" + }, + "title": "Response Admin Partner Tools Import", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { "content": { "application/json": { "schema": { @@ -12680,7 +14086,17 @@ } } }, - "description": "Log storage unavailable" + "description": "Request validation failed" + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid or unauthorized request" }, "5XX": { "content": { @@ -12698,52 +14114,35 @@ "APIKeyCookie": [] } ], - "summary": "Runtime Logs", + "summary": "Import Products", "tags": [ - "admin-jobs" + "admin-partner-tools" ] } }, - "/v1/admin/jobs/{kind}/{job_id}/retry": { + "/v1/admin/partner-tools/import/nick": { "post": { - "operationId": "admin_job_retry", - "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" + "operationId": "admin_partner_tools_import_nick", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NickImport" + } } }, - { - "in": "path", - "name": "job_id", - "required": true, - "schema": { - "format": "uuid", - "title": "Job Id", - "type": "string" - } - } - ], + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminJobOut" + "items": { + "$ref": "#/components/schemas/ProductOut" + }, + "title": "Response Admin Partner Tools Import Nick", + "type": "array" } } }, @@ -12785,21 +14184,37 @@ "APIKeyCookie": [] } ], - "summary": "Retry", + "summary": "Import Nick", "tags": [ - "admin-jobs" + "admin-partner-tools" ] } }, - "/v1/admin/notifications/config": { + "/v1/admin/partner-tools/{product_id}/evaluations": { "get": { - "operationId": "admin_notification_config", + "operationId": "admin_partner_tools_evaluations", + "parameters": [ + { + "in": "path", + "name": "product_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Product Id", + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationConfig" + "items": { + "$ref": "#/components/schemas/EvaluationOut" + }, + "title": "Response Admin Partner Tools Evaluations", + "type": "array" } } }, @@ -12841,33 +14256,41 @@ "APIKeyCookie": [] } ], - "summary": "Config", + "summary": "Evaluations", "tags": [ - "admin-notifications" + "admin-partner-tools" ] - } - }, - "/v1/admin/notifications/deliveries/{identifier}/retry": { + }, "post": { - "operationId": "admin_notification_retry", + "operationId": "admin_partner_tools_evaluate", "parameters": [ { "in": "path", - "name": "identifier", + "name": "product_id", "required": true, "schema": { "format": "uuid", - "title": "Identifier", + "title": "Product Id", "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvaluationInput" + } + } + }, + "required": true + }, "responses": { - "200": { + "202": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminJobOut" + "$ref": "#/components/schemas/EvaluationOut" } } }, @@ -12909,35 +14332,53 @@ "APIKeyCookie": [] } ], - "summary": "Retry", + "summary": "Evaluate", "tags": [ - "admin-notifications" + "admin-partner-tools" ] } }, - "/v1/admin/overview": { - "get": { - "operationId": "admin_overview", + "/v1/admin/partner-tools/{product_id}/evaluations/{job_id}/review": { + "post": { + "operationId": "admin_partner_tools_match_review", "parameters": [ { - "in": "query", - "name": "days", - "required": false, + "in": "path", + "name": "product_id", + "required": true, "schema": { - "default": 30, - "maximum": 90, - "minimum": 1, - "title": "Days", - "type": "integer" + "format": "uuid", + "title": "Product Id", + "type": "string" + } + }, + { + "in": "path", + "name": "job_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Job Id", + "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MatchReview" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminOverview" + "$ref": "#/components/schemas/EvaluationOut" } } }, @@ -12979,79 +14420,43 @@ "APIKeyCookie": [] } ], - "summary": "Overview", + "summary": "Review Match", "tags": [ - "admin-overview" + "admin-partner-tools" ] } }, - "/v1/admin/overview/panels/{panel}": { - "get": { - "operationId": "admin_overview_panel", + "/v1/admin/partner-tools/{product_id}/review": { + "post": { + "operationId": "admin_partner_tools_review", "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" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductReview" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OverviewPanel" + "$ref": "#/components/schemas/ProductOut" } } }, @@ -13093,9 +14498,9 @@ "APIKeyCookie": [] } ], - "summary": "Overview Panel", + "summary": "Review Product", "tags": [ - "admin-overview" + "admin-partner-tools" ] } }, diff --git a/apps/admin/orval.config.ts b/apps/admin/orval.config.ts index a7ef629c8..f3a6a3720 100644 --- a/apps/admin/orval.config.ts +++ b/apps/admin/orval.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ tags: [ "admin-auth", "admin-users", + "admin-partner-tools", "admin-settings", "admin-ai-connection", "admin-overview", 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..730fd6bae --- /dev/null +++ b/apps/admin/src/app/(admin)/partner-tools/page.tsx @@ -0,0 +1,7 @@ +import type { Metadata } from "next"; +import { PartnerTools } from "@/components/organisms/partner-tools"; + +export const metadata: Metadata = { title: "Partner tools" }; +export default function PartnerToolsPage() { + return ; +} 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..dbf9165d9 --- /dev/null +++ b/apps/admin/src/components/organisms/partner-tools.tsx @@ -0,0 +1,760 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/atoms/button"; +import { Input } from "@/components/atoms/input"; +import { Textarea } from "@/components/atoms/textarea"; +import { Field } from "@/components/molecules/field"; +import { Select } from "@/components/molecules/select"; +import { PageHeading } from "@/components/molecules/page-heading"; +import { RequestState } from "@/components/molecules/request-state"; +import { ApiError } from "@/lib/api/client"; +import { useAdmin } from "@/components/molecules/admin-session"; +import { + adminPartnerToolsList, + adminPartnerToolsImport, + adminPartnerToolsImportNick, + adminPartnerToolsReview, + adminPartnerToolsEvaluate, + adminPartnerToolsEvaluations, + adminPartnerToolsMatchReview, +} from "@/lib/api/generated/admin"; +import type { + ProductInput, + NickProduct, + ProductOut, + EvaluationOut, + ProductReview, +} from "@/lib/api/generated/models"; + +const empty: ProductInput = { + provider: "nick-launches", + external_id: "", + name: "", + product_url: "", + listing_url: "", + description: "", + pricing: "unknown", + technologies: [], + evidence: [], + attribution: "Via Nick Launches", +}; +const asError = (error: unknown) => { + if (error instanceof ApiError && Object.keys(error.fields).length) + return new Error( + Object.entries(error.fields) + .map(([field, message]) => `${field}: ${message}`) + .join("; "), + ); + return error instanceof Error ? error : new Error("Request failed. Try again."); +}; + +export function PartnerTools() { + const admin = useAdmin(); + const options = { headers: { "X-CSRF-Token": admin.csrf_token } }; + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [offset, setOffset] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(); + const [busy, setBusy] = useState(false); + const [selected, setSelected] = useState(); + const [draft, setDraft] = useState(empty); + const [editing, setEditing] = useState(false); + const [importText, setImportText] = useState(""); + const [importFormat, setImportFormat] = useState("nick"); + const [runs, setRuns] = useState([]); + const [note, setNote] = useState(""); + const [checked, setChecked] = useState(false); + const [rights, setRights] = useState(false); + const [articleIds, setArticleIds] = useState(""); + const [message, setMessage] = useState(""); + + async function load(page = offset) { + const response = await adminPartnerToolsList({ offset: page, limit: 25 }); + setItems(response.items); + setTotal(response.total); + setSelected((current) => + current ? (response.items.find((item) => item.id === current.id) ?? current) : current, + ); + } + useEffect(() => { + let live = true; + adminPartnerToolsList({ offset, limit: 25 }) + .then((page) => { + if (live) { + setItems(page.items); + setTotal(page.total); + } + }) + .catch((error) => { + if (live) setError(asError(error)); + }) + .finally(() => { + if (live) setLoading(false); + }); + return () => { + live = false; + }; + }, [offset]); + + async function action(work: () => Promise) { + setBusy(true); + setError(undefined); + setMessage(""); + try { + await work(); + } catch (error) { + setError(asError(error)); + } finally { + setBusy(false); + } + } + async function select(product: ProductOut) { + setSelected(product); + setEditing(false); + setRuns([]); + setNote(""); + setChecked(false); + setRights(false); + await action(async () => setRuns(await adminPartnerToolsEvaluations(product.id))); + } + function edit(product?: ProductOut) { + setDraft( + product + ? { + provider: product.provider, + external_id: product.external_id, + name: product.name, + product_url: product.product_url, + listing_url: product.listing_url, + description: product.description, + pricing: product.pricing, + technologies: product.technologies, + evidence: product.evidence, + attribution: product.attribution, + } + : { ...empty, technologies: [], evidence: [] }, + ); + setEditing(true); + } + async function review(status: ProductReview["status"]) { + if (!selected) return; + await action(async () => { + const product = await adminPartnerToolsReview( + selected.id, + { + status, + expected_revision: selected.revision, + note, + evidence_checked: checked, + display_rights_confirmed: rights, + }, + options, + ); + setSelected(product); + setNote(""); + setChecked(false); + setRights(false); + setRuns(await adminPartnerToolsEvaluations(product.id)); + await load(); + setMessage(`Product ${status}. Reader visibility remains off.`); + }); + } + const textField = ( + key: "provider" | "external_id" | "name" | "product_url" | "listing_url" | "attribution", + label: string, + type = "text", + ) => ( + + {(control) => ( + setDraft({ ...draft, [key]: e.target.value })} + /> + )} + + ); + return ( +
+ +

+ Evaluation only. Products and matches are private. Approval does not publish a product or + change the reader feed. +

+ void action(() => load())} /> + {message &&

{message}

} +
+ + +
+
+ Import a product collection +
{ + event.preventDefault(); + void action(async () => { + if (new TextEncoder().encode(importText).length > 1_000_000) + throw new Error("Use a collection smaller than 1 MB."); + const parsed: unknown = JSON.parse(importText); + const raw = + parsed && typeof parsed === "object" && "results" in parsed + ? parsed.results + : parsed; + if (!Array.isArray(raw)) + throw new Error("Provide a JSON array or a Nick API response with results."); + if (importFormat === "nick") + await adminPartnerToolsImportNick({ products: raw as NickProduct[] }, options); + else await adminPartnerToolsImport({ products: raw as ProductInput[] }, options); + setImportText(""); + await load(); + setMessage("Collection imported for review."); + }); + }} + > +

+ Paste up to 50 products using the fields in the product form. Existing provider and + external ID pairs are updated; changed products require verification again. +

+ + {(control) => ( +