From d31ea8ff2599a9b72c290b870d8f990f0b280328 Mon Sep 17 00:00:00 2001 From: hyungi Date: Thu, 25 Jun 2026 08:31:46 +0000 Subject: [PATCH] =?UTF-8?q?feat(publish):=20P1-2=20=EA=B0=80=EA=B3=B5?= =?UTF-8?q?=ED=98=84=ED=99=A9=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=8A=A4?= =?UTF-8?q?=EB=83=85=EC=83=B7=20API=20+=20P1-4=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=ED=94=8C=EB=9E=98=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/api/published.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ app/core/config.py | 6 +++++ docker-compose.yml | 2 ++ 3 files changed, 64 insertions(+) diff --git a/app/api/published.py b/app/api/published.py index a8ff663..362baed 100644 --- a/app/api/published.py +++ b/app/api/published.py @@ -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, + }, + } diff --git a/app/core/config.py b/app/core/config.py index bdb1aad..5f70fb6 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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, diff --git a/docker-compose.yml b/docker-compose.yml index 111d186..d00f044 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -213,6 +213,8 @@ services: # docsrv-viewer-publish: 발행 워커/저작 enqueue 게이트(기본 false=inert) + 뷰어↔DS feed Bearer. - STUDY_PUBLISH_ENABLED=${STUDY_PUBLISH_ENABLED:-false} - DIGEST_PUBLISH_ENABLED=${DIGEST_PUBLISH_ENABLED:-false} + - MAINTENANCE_MODE=${MAINTENANCE_MODE:-false} + - MAINTENANCE_NOTE=${MAINTENANCE_NOTE:-} - VIEWER_SYNC_TOKEN=${VIEWER_SYNC_TOKEN:-} # study-to-viewer P2: 뷰어 write-back ingest 게이트(기본 false=inert, 검증 후 점등). - STUDY_INGEST_ENABLED=${STUDY_INGEST_ENABLED:-false}