diff --git a/app/api/routers/admin_dbops.py b/app/api/routers/admin_dbops.py index 0f4d1f6..ce63ea2 100644 --- a/app/api/routers/admin_dbops.py +++ b/app/api/routers/admin_dbops.py @@ -5,7 +5,7 @@ import logging from fastapi import APIRouter, Depends, Path, Query, Request from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import get_api_admin_user +from app.api.deps import get_api_admin_user, get_api_current_user from app.api.errors import ApiException from app.api.responses import success_payload from app.api.schemas import ( @@ -185,7 +185,7 @@ async def get_pdf_queue( offset: int | None = Query(default=None, ge=0), status: str | None = Query(default=None), db_session: AsyncSession = Depends(get_db_session), - admin_user: User = Depends(get_api_admin_user), + current_user: User = Depends(get_api_current_user), ): normalized_status = (status or "").strip().lower() or None resolved_page, resolved_limit, resolved_offset = _resolve_pdf_queue_paging( @@ -204,7 +204,7 @@ async def get_pdf_queue( logger, "info", "api.admin.db.pdf_queue_listed", - admin_user_id=int(admin_user.id), + user_id=int(current_user.id), page=int(resolved_page), page_size=int(resolved_limit), offset=int(resolved_offset), diff --git a/frontend/src/components/patterns/ScrapeSafetyBadge.test.ts b/frontend/src/components/patterns/ScrapeSafetyBadge.test.ts new file mode 100644 index 0000000..8d17969 --- /dev/null +++ b/frontend/src/components/patterns/ScrapeSafetyBadge.test.ts @@ -0,0 +1,61 @@ +// @vitest-environment happy-dom +import { describe, expect, it } from "vitest"; +import { mount } from "@vue/test-utils"; +import ScrapeSafetyBadge from "./ScrapeSafetyBadge.vue"; +import { createDefaultSafetyState, type ScrapeSafetyState } from "@/features/safety"; + +function buildState(overrides: Partial = {}): ScrapeSafetyState { + return { ...createDefaultSafetyState(), ...overrides }; +} + +describe("ScrapeSafetyBadge", () => { + it("shows ready tooltip when cooldown is inactive", () => { + const wrapper = mount(ScrapeSafetyBadge, { + props: { state: buildState({ cooldown_active: false }) }, + }); + expect(wrapper.text()).toContain("Safety ready"); + const hint = wrapper.findComponent({ name: "AppHelpHint" }); + expect(hint.exists()).toBe(true); + expect(hint.props("text")).toContain("No active cooldown"); + }); + + it("shows cooldown tooltip with reason and action when active", () => { + const wrapper = mount(ScrapeSafetyBadge, { + props: { + state: buildState({ + cooldown_active: true, + cooldown_reason: "blocked_failure_threshold_exceeded", + cooldown_reason_label: "Too many blocked requests", + recommended_action: "Wait for cooldown to expire", + cooldown_remaining_seconds: 120, + }), + }, + }); + expect(wrapper.text()).toContain("Safety cooldown"); + const hint = wrapper.findComponent({ name: "AppHelpHint" }); + expect(hint.exists()).toBe(true); + const text = hint.props("text") as string; + expect(text).toContain("Google Scholar rate-limits"); + expect(text).toContain("Too many blocked requests"); + expect(text).toContain("Wait for cooldown to expire"); + }); + + it("shows cooldown tooltip without optional fields", () => { + const wrapper = mount(ScrapeSafetyBadge, { + props: { + state: buildState({ + cooldown_active: true, + cooldown_reason: "some_reason", + cooldown_reason_label: null, + recommended_action: null, + cooldown_remaining_seconds: 60, + }), + }, + }); + const hint = wrapper.findComponent({ name: "AppHelpHint" }); + const text = hint.props("text") as string; + expect(text).toContain("Google Scholar rate-limits"); + expect(text).not.toContain("Why:"); + expect(text).not.toContain("Action:"); + }); +}); diff --git a/frontend/src/components/patterns/ScrapeSafetyBadge.vue b/frontend/src/components/patterns/ScrapeSafetyBadge.vue index b40c1c9..845fb2f 100644 --- a/frontend/src/components/patterns/ScrapeSafetyBadge.vue +++ b/frontend/src/components/patterns/ScrapeSafetyBadge.vue @@ -1,6 +1,7 @@ diff --git a/frontend/src/components/patterns/ScrapeSafetyBanner.vue b/frontend/src/components/patterns/ScrapeSafetyBanner.vue index 300b05a..6ea343d 100644 --- a/frontend/src/components/patterns/ScrapeSafetyBanner.vue +++ b/frontend/src/components/patterns/ScrapeSafetyBanner.vue @@ -2,6 +2,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import AppAlert from "@/components/ui/AppAlert.vue"; +import AppHelpHint from "@/components/ui/AppHelpHint.vue"; import { formatCooldownCountdown, type ScrapeSafetyState, @@ -79,6 +80,10 @@ const actionText = computed(() => { return props.safetyState.recommended_action; }); +const bannerHintText = computed(() => { + return "Google Scholar rate-limits automated requests. The cooldown pauses scraping to avoid your IP being blocked."; +}); + onMounted(() => { timer = setInterval(() => { now.value = Date.now(); @@ -95,7 +100,12 @@ onBeforeUnmount(() => {