feat(publish): P1-2 가공현황 라이브 스냅샷 API + P1-4 점검 플래그
GET /published/processing-status (Bearer, read-only, pull-through) — build_overview 재사용 + source_health⋈news_sources 요약(by_circuit_state·problems). 저장 X(라이브), 소비자 2~3s timeout 책임. P1-4: MAINTENANCE_MODE/NOTE 플래그 동봉 — 소프트락/점검이 워커 멈춰 수치 정체 시 뷰어가 배너로 구분(표면 != 데이터). 검증: 무토큰 401·유효 200 (overview+sources 67 closed+maintenance off). docsrv-viewer-publish (plan P1-2/P1-4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
import logging
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
@@ -25,6 +27,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from core.config import settings
|
||||
from core.database import async_session
|
||||
from models.published import Published
|
||||
from models.published import Published
|
||||
from services.queue_overview import build_overview
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -196,3 +200,55 @@ async def published_digest(
|
||||
next_since=next_since,
|
||||
has_more=has_more,
|
||||
)
|
||||
|
||||
|
||||
# ── P1-2: 가공현황 라이브 스냅샷 API (+P1-4 점검 플래그) ──────────────────────────
|
||||
# 뷰어 리포트 '문서 가공현황' 섹션용. build_overview(기존 서비스) 재사용 + source_health
|
||||
# 조인 요약. pull-through(저장 X) — 라이브 수치라 캐시 없음, 소비자(뷰어)가 2~3s timeout 책임
|
||||
# (plan P1-2). P1-4: maintenance 플래그 동봉 — 소프트락/점검이 워커를 멈춰 수치가 정체로
|
||||
# 보일 때 뷰어가 '점검·실험 중' 배너로 구분(표면 != 데이터). read-only.
|
||||
@router.get("/processing-status")
|
||||
async def published_processing_status(
|
||||
_auth: None = Depends(_verify_token),
|
||||
session: AsyncSession = Depends(_session),
|
||||
):
|
||||
"""가공현황 스냅샷: queue overview + source_health 요약 + maintenance 플래그."""
|
||||
overview = await build_overview(session)
|
||||
|
||||
sh_rows = (await session.execute(text(
|
||||
"SELECT ns.name, ns.category, sh.circuit_state, sh.consecutive_failures, sh.empty_streak, "
|
||||
"sh.last_success_at, sh.last_probe_ok "
|
||||
"FROM source_health sh JOIN news_sources ns ON ns.id = sh.source_id "
|
||||
"ORDER BY (sh.circuit_state <> 'closed') DESC, sh.consecutive_failures DESC"
|
||||
))).mappings().all()
|
||||
|
||||
by_state: dict[str, int] = {}
|
||||
problems: list[dict] = []
|
||||
for r in sh_rows:
|
||||
st = r["circuit_state"]
|
||||
by_state[st] = by_state.get(st, 0) + 1
|
||||
if st != "closed":
|
||||
problems.append({
|
||||
"name": r["name"],
|
||||
"category": r["category"],
|
||||
"circuit_state": st,
|
||||
"consecutive_failures": r["consecutive_failures"],
|
||||
"empty_streak": r["empty_streak"],
|
||||
"last_success_at": r["last_success_at"].isoformat() if r["last_success_at"] else None,
|
||||
"last_probe_ok": r["last_probe_ok"],
|
||||
})
|
||||
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"overview": overview,
|
||||
"sources": {
|
||||
"total": len(sh_rows),
|
||||
"by_circuit_state": by_state,
|
||||
"problems": problems,
|
||||
},
|
||||
"maintenance": {
|
||||
"active": settings.maintenance_mode,
|
||||
"note": settings.maintenance_note,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -188,6 +188,8 @@ class Settings(BaseModel):
|
||||
# 발행 레이어(docsrv-viewer-publish): publish_outbox 워커 게이트. 저자/4-A enqueue 결선(P0-1b) 후 true.
|
||||
study_publish_enabled: bool = False
|
||||
digest_publish_enabled: bool = False # docsrv-viewer-publish P1-1 (뉴스/다이제스트 발행 feed gate)
|
||||
maintenance_mode: bool = False # P1-4: 점검/실험 중 = 가공현황 배너(표면 != 데이터)
|
||||
maintenance_note: str = ""
|
||||
# 뷰어 write-back ingest(study-to-viewer P2) 게이트. /ingest/study/attempts 활성. 기본 false=inert(503).
|
||||
study_ingest_enabled: bool = False
|
||||
|
||||
@@ -206,6 +208,8 @@ def load_settings() -> Settings:
|
||||
study_card_extract_enabled = os.getenv("STUDY_CARD_EXTRACT_ENABLED", "true").lower() in ("1", "true", "yes")
|
||||
study_publish_enabled = os.getenv("STUDY_PUBLISH_ENABLED", "false").lower() in ("1", "true", "yes")
|
||||
digest_publish_enabled = os.getenv("DIGEST_PUBLISH_ENABLED", "false").lower() in ("1", "true", "yes")
|
||||
maintenance_mode = os.getenv("MAINTENANCE_MODE", "false").lower() in ("1", "true", "yes")
|
||||
maintenance_note = os.getenv("MAINTENANCE_NOTE", "")
|
||||
study_ingest_enabled = os.getenv("STUDY_INGEST_ENABLED", "false").lower() in ("1", "true", "yes")
|
||||
internal_worker_token = os.getenv("INTERNAL_WORKER_TOKEN", "")
|
||||
viewer_sync_token = os.getenv("VIEWER_SYNC_TOKEN", "")
|
||||
@@ -343,6 +347,8 @@ def load_settings() -> Settings:
|
||||
study_card_extract_enabled=study_card_extract_enabled,
|
||||
study_publish_enabled=study_publish_enabled,
|
||||
digest_publish_enabled=digest_publish_enabled,
|
||||
maintenance_mode=maintenance_mode,
|
||||
maintenance_note=maintenance_note,
|
||||
study_ingest_enabled=study_ingest_enabled,
|
||||
internal_worker_token=internal_worker_token,
|
||||
viewer_sync_token=viewer_sync_token,
|
||||
|
||||
Reference in New Issue
Block a user