diff --git a/apps/admin/src/components/organisms/overview-breakdown-charts.tsx b/apps/admin/src/components/organisms/overview-breakdown-charts.tsx index 00e6d06db..d4c4d8b31 100644 --- a/apps/admin/src/components/organisms/overview-breakdown-charts.tsx +++ b/apps/admin/src/components/organisms/overview-breakdown-charts.tsx @@ -1,18 +1,7 @@ "use client"; -import { - Bar, - BarChart, - CartesianGrid, - Cell, - Scatter, - ScatterChart, - Tooltip, - XAxis, - YAxis, - ZAxis, -} from "recharts"; -import { ChartContainer, ChartTooltip } from "@/components/atoms/chart"; +import { CartesianGrid, Cell, Scatter, ScatterChart, Tooltip, XAxis, YAxis, ZAxis } from "recharts"; +import { ChartContainer } from "@/components/atoms/chart"; export type DistributionRow = { label: string; value: number; color: string; detail?: string }; export function DistributionChart({ @@ -186,74 +175,3 @@ export function CoverageChart({ ); } - -export function JobOutcomesChart({ - rows, -}: { - rows: { name: string; completed: number; failed: number }[]; -}) { - const active = rows.filter((row) => row.completed + row.failed > 0); - if (!active.length) - return ( -
- No completed or failed jobs in this period. -
- ); - return ( - <> -Sign-in provider
" }), @@ -191,6 +192,8 @@ test( } else if (url.pathname === "/api/v1/user/auth/me") { json = null; } else if (url.pathname === "/api/v1/feed/options") { + if (feedOptionsStatus !== 200) + return route.fulfill({ status: feedOptionsStatus, json: {} }); json = { sources: article.sources, content_types: ["article", "news", "tutorial", "release", "comparison", "opinion"], @@ -261,6 +264,29 @@ test( await checkArticleGrid(page, path.join(extension, "../grid-" + browser)); assert.ok(page.url().startsWith("chrome-extension://")); await page.waitForURL(/#\/latest$/); + for (const status of [404, 503]) { + feedOptionsStatus = status; + const sourceRequests = requests.filter((url) => url.pathname === "/api/v1/sources").length; + const optionsResponse = page.waitForResponse( + (response) => new URL(response.url()).pathname === "/api/v1/feed/options", + ); + await page.reload(); + assert.equal((await optionsResponse).status(), status); + await page.locator(".article-card").first().waitFor(); + assert.deepEqual(await page.locator(".feed-toolbar a").allTextContents(), ["All"]); + assert.equal( + requests.filter((url) => url.pathname === "/api/v1/sources").length, + sourceRequests, + "Unavailable feed options must not trigger a broad source-catalog fallback", + ); + } + feedOptionsStatus = 200; + await page.reload(); + await page + .locator(".feed-toolbar") + .getByRole("link", { name: "News", exact: true }) + .waitFor(); + await page.locator(".article-card").first().waitFor(); const publicProfile = await context.newPage(); await publicProfile.goto(page.url().split("#")[0] + "#/users/reader"); await publicProfile.getByRole("heading", { name: "Reader Profile" }).waitFor(); diff --git a/apps/web/src/lib/mcp.ts b/apps/web/src/lib/mcp.ts index 94ee45b1c..66b9ef3ef 100644 --- a/apps/web/src/lib/mcp.ts +++ b/apps/web/src/lib/mcp.ts @@ -1,8 +1,5 @@ export type McpClient = "vscode" | "codex" | "claude" | "cursor" | "other"; -export const mcpServerGuide = - "https://github.com/abhi1693/devfeed.tech/blob/master/apps/mcp/README.md"; - export const mcpClients: Record< McpClient, { diff --git a/packages/core/src/job_retries.py b/packages/core/src/job_retries.py index b45402e30..731d49f22 100644 --- a/packages/core/src/job_retries.py +++ b/packages/core/src/job_retries.py @@ -1,6 +1,6 @@ """Explicit retries use current eligibility and the pipelines' coalescing requests.""" -from sqlalchemy import and_, case, or_, select, tuple_ +from sqlalchemy import and_, or_, select, tuple_ from sqlalchemy.orm import aliased from devfeed_core.analysis import request_analysis @@ -58,13 +58,6 @@ def retry_candidate(model): return and_(failed, ~superseded) -def job_display_status(model): - return case( - (and_(model.status == "failed", ~retry_candidate(model)), "retried"), - else_=model.status, - ) - - def retry_notification(session, identifier): job = session.scalar( select(NotificationDelivery) diff --git a/packages/core/src/schemas.py b/packages/core/src/schemas.py index 7ad47fe9d..32e90ce58 100644 --- a/packages/core/src/schemas.py +++ b/packages/core/src/schemas.py @@ -191,13 +191,6 @@ class SourcePublicOut(SourceRef): created_at: datetime -class SourceSubmissionOut(SourceRef): - feed_url: str - approval_status: ApprovalStatus - submitted_by: SourceSubmitterOut | None - created_at: datetime - - class ImageJobOut(ORMModel): id: uuid.UUID article_id: uuid.UUID | None diff --git a/packages/core/src/topic_corrections.py b/packages/core/src/topic_corrections.py index f62159ec4..f1ff26f70 100644 --- a/packages/core/src/topic_corrections.py +++ b/packages/core/src/topic_corrections.py @@ -2,94 +2,16 @@ import uuid -from sqlalchemy import or_, select +from sqlalchemy import select from devfeed_core.analysis import snapshot_hash -from devfeed_core.models import ArticleTopic, Tag, Topic, TopicProposal, TopicRelation, utcnow +from devfeed_core.models import Topic, TopicProposal from devfeed_core.services import OperationConflict, RecordNotFound from devfeed_core.topic_auto_approval import ACTOR from devfeed_core.topic_proposals import TopicReview, review_proposal, snapshot from devfeed_core.topics import TopicWrite, lock_topics -def retract_automatic_topic(session, identifier, *, expected_hash, note, actor): - """Retire an unchanged automatic approval after its links have been reconciled. - - Retain the rejected identity so imports cannot silently recreate it, and retain - the complete previous review in the proposal's correction evidence. - """ - if not note.strip() or len(note) > 1000: - raise ValueError("A bounded correction reason is required") - lock_topics(session) - topic = session.scalar( - select(Topic) - .where(Topic.id == identifier) - .with_for_update(nowait=True) - .execution_options(populate_existing=True) - ) - if topic is None: - raise RecordNotFound("Topic not found") - before = snapshot(topic) - if topic.status != "active" or snapshot_hash(before) != expected_hash: - raise OperationConflict("Topic changed before retraction") - previous = session.scalar( - select(TopicProposal) - .where(TopicProposal.topic_id == identifier, TopicProposal.status == "approved") - .order_by(TopicProposal.reviewed_at.desc(), TopicProposal.id.desc()) - .limit(1) - .with_for_update(nowait=True) - ) - if ( - previous is None - or previous.reviewed_at is None - or previous.reviewed_by != ACTOR - or previous.applied != before - ): - raise OperationConflict("Only an unchanged automatic approval can be retracted") - if ( - session.scalar(select(ArticleTopic.article_id).where(ArticleTopic.topic_id == identifier)) - or session.scalar(select(Tag.id).where(Tag.topic_id == identifier)) - or session.scalar( - select(TopicRelation.topic_id).where( - or_( - TopicRelation.topic_id == identifier, - TopicRelation.related_topic_id == identifier, - ) - ) - ) - or session.scalar( - select(TopicProposal.id).where( - TopicProposal.topic_id == identifier, TopicProposal.status == "pending" - ) - ) - ): - raise OperationConflict( - "Reconcile article, tag, relationship and pending proposal links first" - ) - now = utcnow() - previous.evidence = [ - *previous.evidence, - { - "provider": "authorized_topic_retraction", - "actor": actor, - "reason": note, - "corrected_at": now.isoformat(), - "previous_status": previous.status, - "previous_reviewed_at": previous.reviewed_at.isoformat(), - "previous_reviewed_by": previous.reviewed_by, - "previous_review_note": previous.review_note, - "previous_snapshot": before, - }, - ] - previous.status, previous.reviewed_at, previous.reviewed_by = "rejected", now, actor - previous.review_note = note - topic.status = "rejected" - from devfeed_core.tag_topic_discovery import catalog_changed - - catalog_changed(session) - session.flush() - - def correct_automatic_topic(session, identifier, *, expected_hash, body: TopicWrite, note, actor): """Caller supplies an explicitly reviewed replacement and expected current snapshot.""" if not note.strip() or len(note) > 1000: diff --git a/tests/test_job_retry_unit.py b/tests/test_job_retry_unit.py index 45632b75a..6340169ea 100644 --- a/tests/test_job_retry_unit.py +++ b/tests/test_job_retry_unit.py @@ -73,14 +73,8 @@ def row(number, owner, status="failed", age=0): eligible = set( connection.scalars(select(model.id).where(job_retries.retry_candidate(model))) ) - display = dict( - connection.execute(select(model.id, job_retries.job_display_status(model))).all() - ) engine.dispose() assert eligible == {uuid.UUID(int=2), uuid.UUID(int=9)} - assert [display[uuid.UUID(int=index)] for index in (1, 3, 5, 7)] == ["retried"] * 4 - assert display[uuid.UUID(int=6)] == "running" - assert display[uuid.UUID(int=8)] == "queued" def test_notification_retry_selection_reuses_each_failed_delivery():